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

# Conflicts:
#	packages/bruno-lang/v2/src/jsonToBru.js
This commit is contained in:
busy-panda 2024-09-09 11:49:38 +02:00
commit 62babef678
307 changed files with 10004 additions and 20657 deletions

View File

@ -15,6 +15,8 @@ jobs:
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version-file: '.nvmrc' node-version-file: '.nvmrc'
cache: 'npm'
cache-dependency-path: './package-lock.json'
- name: Install dependencies - name: Install dependencies
run: npm ci --legacy-peer-deps run: npm ci --legacy-peer-deps
@ -23,8 +25,14 @@ jobs:
run: | run: |
npm run build --workspace=packages/bruno-common npm run build --workspace=packages/bruno-common
npm run build --workspace=packages/bruno-query npm run build --workspace=packages/bruno-query
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
# tests
- name: Test Package bruno-js
run: npm run test --workspace=packages/bruno-js
- name: Test Package bruno-cli
run: npm run test --workspace=packages/bruno-cli
# test
- name: Test Package bruno-query - name: Test Package bruno-query
run: npm run test --workspace=packages/bruno-query run: npm run test --workspace=packages/bruno-query
- name: Test Package bruno-lang - name: Test Package bruno-lang
@ -33,12 +41,8 @@ jobs:
run: npm run test --workspace=packages/bruno-schema run: npm run test --workspace=packages/bruno-schema
- name: Test Package bruno-app - name: Test Package bruno-app
run: npm run test --workspace=packages/bruno-app run: npm run test --workspace=packages/bruno-app
- name: Test Package bruno-js
run: npm run test --workspace=packages/bruno-js
- name: Test Package bruno-common - name: Test Package bruno-common
run: npm run test --workspace=packages/bruno-common run: npm run test --workspace=packages/bruno-common
- name: Test Package bruno-cli
run: npm run test --workspace=packages/bruno-cli
- name: Test Package bruno-electron - name: Test Package bruno-electron
run: npm run test --workspace=packages/bruno-electron run: npm run test --workspace=packages/bruno-electron
@ -50,6 +54,8 @@ jobs:
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version-file: '.nvmrc' node-version-file: '.nvmrc'
cache: 'npm'
cache-dependency-path: './package-lock.json'
- name: Install dependencies - name: Install dependencies
run: npm ci --legacy-peer-deps run: npm ci --legacy-peer-deps
@ -58,6 +64,7 @@ jobs:
run: | run: |
npm run build --workspace=packages/bruno-query npm run build --workspace=packages/bruno-query
npm run build --workspace=packages/bruno-common npm run build --workspace=packages/bruno-common
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
- name: Run tests - name: Run tests
run: | run: |
@ -71,15 +78,3 @@ jobs:
with: with:
files: packages/bruno-tests/collection/junit.xml files: packages/bruno-tests/collection/junit.xml
comment_mode: always comment_mode: always
prettier:
name: Prettier
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
- name: Install dependencies
run: npm ci --legacy-peer-deps
- name: Run Prettier
run: npm run test:prettier:web

View File

@ -1,4 +0,0 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx pretty-quick --staged

View File

@ -34,10 +34,11 @@ Libraries we use
- Schema Validation - Yup - Schema Validation - Yup
- Request Client - axios - Request Client - axios
- Filesystem Watcher - chokidar - Filesystem Watcher - chokidar
- i18n - i18next
### Dependencies ### Dependencies
You would need [Node v18.x or the latest LTS version](https://nodejs.org/en/) and npm 8.x. We use npm workspaces in the project You would need [Node v20.x or the latest LTS version](https://nodejs.org/en/) and npm 8.x. We use npm workspaces in the project
## Development ## Development
@ -57,6 +58,9 @@ npm run build:graphql-docs
npm run build:bruno-query npm run build:bruno-query
npm run build:bruno-common npm run build:bruno-common
# bundle js sandbox libraries
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
# run next app (terminal 1) # run next app (terminal 1)
npm run dev:web npm run dev:web

View File

@ -37,7 +37,7 @@ Bruno 基于 NextJs 和 React 构建。我们使用 Electron 来封装桌面版
### 依赖项 ### 依赖项
您需要 [Node v18.x 或最新的 LTS 版本](https://nodejs.org/en/) 和 npm 8.x。我们在这个项目中也使用 npm 工作区_npm workspaces_ 您需要 [Node v20.x 或最新的 LTS 版本](https://nodejs.org/en/) 和 npm 8.x。我们在这个项目中也使用 npm 工作区_npm workspaces_
## 开发 ## 开发

View File

@ -37,7 +37,7 @@ Bibliotheken die wir benutzen
### Abhängigkeiten ### Abhängigkeiten
Du benötigst [Node v18.x oder die neuste LTS Version](https://nodejs.org/en/) und npm 8.x. Wir benutzen npm workspaces in dem Projekt. Du benötigst [Node v20.x oder die neuste LTS Version](https://nodejs.org/en/) und npm 8.x. Wir benutzen npm workspaces in dem Projekt.
### Lass uns coden ### Lass uns coden

View File

@ -37,7 +37,7 @@ Librerías que utilizamos:
### Dependencias ### Dependencias
Necesitarás [Node v18.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. 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.
## Desarrollo ## Desarrollo

View File

@ -37,7 +37,7 @@ Les librairies que nous utilisons :
### Dépendances ### Dépendances
Vous aurez besoin de [Node v18.x ou la dernière version LTS](https://nodejs.org/en/) et npm 8.x. Nous utilisons aussi les espaces de travail npm (_npm workspaces_) dans ce projet. Vous aurez besoin de [Node v20.x ou la dernière version LTS](https://nodejs.org/en/) et npm 8.x. Nous utilisons aussi les espaces de travail npm (_npm workspaces_) dans ce projet.
## Développement ## Développement
@ -62,6 +62,9 @@ npm run build:graphql-docs
# construction de bruno query # construction de bruno query
npm run build:bruno-query npm run build:bruno-query
# construction de bruno common
npm run build:bruno-common
# démarrage de next (terminal 1) # démarrage de next (terminal 1)
npm run dev:web npm run dev:web

View File

@ -37,7 +37,7 @@ Libraries जिनका हम उपयोग करते हैं
### निर्भरताएँ ### निर्भरताएँ
आपको [Node v18.x या नवीनतम LTS संस्करण](https://nodejs.org/en/) और npm 8.x की आवश्यकता होगी। हम प्रोजेक्ट में npm वर्कस्पेस का उपयोग करते हैं आपको [Node v20.x या नवीनतम LTS संस्करण](https://nodejs.org/en/) और npm 8.x की आवश्यकता होगी। हम प्रोजेक्ट में npm वर्कस्पेस का उपयोग करते हैं
## डेवलपमेंट ## डेवलपमेंट

View File

@ -37,7 +37,7 @@ Le librerie che utilizziamo sono:
### Dependences ### Dependences
Hai bisogno di [Node v18.x o dell'ultima versione LTS](https://nodejs.org/en/) di npm 8.x. Utilizziamo gli spazi di lavoro npm (_npm workspaces_) in questo progetto. Hai bisogno di [Node v20.x o dell'ultima versione LTS](https://nodejs.org/en/) di npm 8.x. Utilizziamo gli spazi di lavoro npm (_npm workspaces_) in questo progetto.
### Iniziamo a codificare ### Iniziamo a codificare

View File

@ -37,7 +37,7 @@ Bruno は Next.js と React で作られています。デスクトップアプ
### 依存関係 ### 依存関係
[Node v18.x もしくは最新の LTS バージョン](https://nodejs.org/en/)と npm 8.x が必要です。プロジェクトに npm ワークスペースを使用しています。 [Node v20.x もしくは最新の LTS バージョン](https://nodejs.org/en/)と npm 8.x が必要です。プロジェクトに npm ワークスペースを使用しています。
## 開発 ## 開発

View File

@ -37,7 +37,7 @@ Bruno는 Next.js와 React로 구축되었습니다. 또한, (로컬 컬렉션을
### 의존성 ### 의존성
[Node v18.x 혹은 최신 LTS version](https://nodejs.org/en/)과 npm 8.x 버전이 필요합니다. 우리는 이 프로젝트에서 npm workspaces를 사용합니다. [Node v20.x 혹은 최신 LTS version](https://nodejs.org/en/)과 npm 8.x 버전이 필요합니다. 우리는 이 프로젝트에서 npm workspaces를 사용합니다.
## 개발 ## 개발

View File

@ -37,7 +37,7 @@ Biblioteki, których używamy
### Zależności ### Zależności
Będziesz potrzebować [Node v18.x lub najnowszej wersji LTS](https://nodejs.org/en/) oraz npm 8.x. W projekcie używamy npm workspaces Będziesz potrzebować [Node v20.x lub najnowszej wersji LTS](https://nodejs.org/en/) oraz npm 8.x. W projekcie używamy npm workspaces
## Rozwój ## Rozwój

View File

@ -37,7 +37,7 @@ Bibliotecas que utilizamos:
### Dependências ### Dependências
Você precisará do [Node v18.x (ou da versão LTS mais recente)](https://nodejs.org/en/) e do npm na versão 8.x. Nós utilizamos npm workspaces no projeto. Você precisará do [Node v20.x (ou da versão LTS mais recente)](https://nodejs.org/en/) e do npm na versão 8.x. Nós utilizamos npm workspaces no projeto.
## Desenvolvimento ## Desenvolvimento

View File

@ -37,7 +37,7 @@ Bibliotecile pe care le folosim
### Dependențele ### Dependențele
Veți avea nevoie de [Node v18.x sau cea mai recentă versiune LTS](https://nodejs.org/en/) și npm 8.x. Noi folosim spații de lucru npm în proiect Veți avea nevoie de [Node v20.x sau cea mai recentă versiune LTS](https://nodejs.org/en/) și npm 8.x. Noi folosim spații de lucru npm în proiect
## Dezvoltarea ## Dezvoltarea

View File

@ -37,7 +37,7 @@ Bruno построен с использованием Next.js и React. Мы т
### Зависимости ### Зависимости
Вам потребуется [Node v18.x или последняя версия LTS](https://nodejs.org/en/) и npm 8.x. В проекте мы используем рабочие пространства npm Вам потребуется [Node v20.x или последняя версия LTS](https://nodejs.org/en/) и npm 8.x. В проекте мы используем рабочие пространства npm
### Приступим к коду ### Приступим к коду

View File

@ -0,0 +1,84 @@
## Urobme bruno lepším, spoločne !!
Sme radi, že chcete zlepšiť bruno. Nižšie sú uvedené pokyny, ako začať s výchovou bruno na vašom počítači.
### Technologický zásobník
Bruno je vytvorené pomocou Next.js a React. Na dodávanie desktopovej verzie (ktorá podporuje lokálne kolekcie) používame aj electron.
Balíčky, ktoré používame:
- CSS - Tailwind
- Editory kódu - Codemirror
- Správa stavu - Redux
- Ikony - Tabler Icons
- Formuláre - formik
- Overovanie schém - Yup
- Klient požiadaviek - axios
- Sledovač súborového systému - chokidar
### Závislosti
Budete potrebovať [NodeJS v18.x alebo najnovšiu verziu LTS](https://nodejs.org/en/) a npm versiu 8.x. V projekte používame pracovné priestory npm
## Vývoj
Bruno sa vyvíja ako desktopová aplikácia. Aplikáciu je potrebné načítať spustením aplikácie Next.js v jednom termináli a potom spustiť aplikáciu electron v inom termináli.
### Závislosti
- NodeJS v18
### Miestny vývoj
```bash
# použite verziu nodejs 18
nvm use
# nainštalovať balíčky
npm i --legacy-peer-deps
# zostaviť balíčky
npm run build:graphql-docs
npm run build:bruno-query
npm run build:bruno-common
# spustite ďalšiu aplikáciu (terminál 1)
npm run dev:web
# spustite aplikáciu electron (terminál 2)
npm run dev:electron
```
### Riešenie problémov
Pri spustení `npm install` sa môžete stretnúť s chybou `Unsupported platform`. Ak chcete túto chybu odstrániť, musíte odstrániť súbory `node_modules`, `package-lock.json` a spustiť `npm install`. Tým by sa mali nainštalovať všetky potrebné balíky potrebné na spustenie aplikácie.
```shell
# Odstrániť node_modules v podadresároch
find ./ -type d -name "node_modules" -print0 | while read -d $'\0' dir; do
rm -rf "$dir"
done
# Odstráňte package-lock v podadresároch
find . -type f -name "package-lock.json" -delete
```
### Testovanie
````bash
# bruno-schema
npm test --workspace=packages/bruno-schema
# bruno-lang
npm test --workspace=packages/bruno-lang
```
### Vyrobenie Pull Request
- Prosím, aby PR boli malé a zamerané na jednu vec
- Prosím, dodržujte formát vytvárania vetiev
- feature/[názov funkcie]: Táto vetva by mala obsahovať zmeny pre konkrétnu funkciu
- Príklad: feature/dark-mode
- bugfix/[názov chyby]: Táto vetva by mala obsahovať iba opravy konkrétnej chyby
- Príklad: bugfix/bug-1

View File

@ -37,7 +37,7 @@ Kullandığımız kütüphaneler
### Bağımlılıklar ### Bağımlılıklar
[Node v18.x veya en son LTS sürümüne](https://nodejs.org/en/) ve npm 8.x'e ihtiyacınız olacaktır. Projede npm çalışma alanlarını kullanıyoruz [Node v20.x veya en son LTS sürümüne](https://nodejs.org/en/) ve npm 8.x'e ihtiyacınız olacaktır. Projede npm çalışma alanlarını kullanıyoruz
## Gelişim ## Gelişim

View File

@ -37,7 +37,7 @@ Bruno побудований на Next.js та React. Також для деск
### Залежності ### Залежності
Вам знадобиться [Node v18.x або остання LTS версія](https://nodejs.org/en/) та npm 8.x. Ми використовуєм npm workspaces в цьому проекті Вам знадобиться [Node v20.x або остання LTS версія](https://nodejs.org/en/) та npm 8.x. Ми використовуєм npm workspaces в цьому проекті
### Починаєм писати код ### Починаєм писати код

View File

@ -37,7 +37,7 @@ Bruno 使用 Next.js 和 React 構建。我們使用 Electron 來封裝及發佈
### 依賴關係 ### 依賴關係
您需要使用 [Node v18.x 或最新的 LTS 版本](https://nodejs.org/en/) 和 npm 8.x。我們在這個專案中使用 npm 工作區_npm workspaces_ 您需要使用 [Node v20.x 或最新的 LTS 版本](https://nodejs.org/en/) 和 npm 8.x。我們在這個專案中使用 npm 工作區_npm workspaces_
## 開發 ## 開發

View File

@ -27,6 +27,7 @@
| [正體中文](./readme_zhtw.md) | [正體中文](./readme_zhtw.md)
| **العربية** | **العربية**
| [日本語](./readme_ja.md) | [日本語](./readme_ja.md)
| [ქართული](./readme_ka.md)
برونو هو عميل API جديد ومبتكر، يهدف إلى ثورة الحالة الحالية التي يمثلها برنامج Postman وأدوات مماثلة هناك. برونو هو عميل API جديد ومبتكر، يهدف إلى ثورة الحالة الحالية التي يمثلها برنامج Postman وأدوات مماثلة هناك.

View File

@ -27,6 +27,7 @@
| [正體中文](./readme_zhtw.md) | [正體中文](./readme_zhtw.md)
| [العربية](./readme_ar.md) | [العربية](./readme_ar.md)
| [日本語](./readme_ja.md) | [日本語](./readme_ja.md)
| [ქართული](./readme_ka.md)
ব্রুনো হল একটি নতুন এবং উদ্ভাবনী API ক্লায়েন্ট, যার লক্ষ্য পোস্টম্যান এবং অনুরূপ সরঞ্জাম দ্বারা প্রতিনিধিত্ব করা স্থিতাবস্থায় বিপ্লব ঘটানো। ব্রুনো হল একটি নতুন এবং উদ্ভাবনী API ক্লায়েন্ট, যার লক্ষ্য পোস্টম্যান এবং অনুরূপ সরঞ্জাম দ্বারা প্রতিনিধিত্ব করা স্থিতাবস্থায় বিপ্লব ঘটানো।

View File

@ -27,6 +27,7 @@
| [正體中文](./readme_zhtw.md) | [正體中文](./readme_zhtw.md)
| [العربية](./readme_ar.md) | [العربية](./readme_ar.md)
| [日本語](./readme_ja.md) | [日本語](./readme_ja.md)
| [ქართული](./readme_ka.md)
Bruno 是一款全新且创新的 API 客户端,旨在颠覆 Postman 和其他类似工具。 Bruno 是一款全新且创新的 API 客户端,旨在颠覆 Postman 和其他类似工具。

View File

@ -27,6 +27,7 @@
| [正體中文](./readme_zhtw.md) | [正體中文](./readme_zhtw.md)
| [العربية](./readme_ar.md) | [العربية](./readme_ar.md)
| [日本語](./readme_ja.md) | [日本語](./readme_ja.md)
| [ქართული](./readme_ka.md)
Bruno ist ein neuer und innovativer API-Client, der den Status Quo von Postman und ähnlichen Tools revolutionieren soll. Bruno ist ein neuer und innovativer API-Client, der den Status Quo von Postman und ähnlichen Tools revolutionieren soll.
@ -38,7 +39,7 @@ Bruno ist ein reines Offline-Tool. Es gibt keine Pläne, Bruno um eine Cloud-Syn
[Download Bruno](https://www.usebruno.com/downloads) [Download Bruno](https://www.usebruno.com/downloads)
📢 Sehen Sie sich unseren Vortrag auf der India FOSS 3.0 Conference [hier](https://www.youtube.com/watch?v=7bSMFpbcPiY) an. 📢 Sieh Dir unseren Vortrag auf der India FOSS 3.0 Conference [hier](https://www.youtube.com/watch?v=7bSMFpbcPiY) an.
![bruno](/assets/images/landing-2.png) <br /><br /> ![bruno](/assets/images/landing-2.png) <br /><br />
@ -47,13 +48,13 @@ Bruno ist ein reines Offline-Tool. Es gibt keine Pläne, Bruno um eine Cloud-Syn
Die meisten unserer Funktionen sind kostenlos und quelloffen. Die meisten unserer Funktionen sind kostenlos und quelloffen.
Wir bemühen uns um ein Gleichgewicht zwischen [Open-Source-Prinzipien und Nachhaltigkeit](https://github.com/usebruno/bruno/discussions/269) Wir bemühen uns um ein Gleichgewicht zwischen [Open-Source-Prinzipien und Nachhaltigkeit](https://github.com/usebruno/bruno/discussions/269)
Sie können die [Golden Edition](https://www.usebruno.com/pricing) vorbestellen ~~$19~~ **$9** ! <br/> Du kannst die [Golden Edition](https://www.usebruno.com/pricing) bestellen **$19**! <br/>
### Installation ### Installation
Bruno ist als Download [auf unserer Website](https://www.usebruno.com/downloads) für Mac, Windows und Linux verfügbar. Bruno ist als Download [auf unserer Website](https://www.usebruno.com/downloads) für Mac, Windows und Linux verfügbar.
Sie können Bruno auch über Paketmanager wie Homebrew, Chocolatey, Scoop, Snap, Flatpak und Apt installieren. Du kannst Bruno auch über Paketmanager wie Homebrew, Chocolatey, Scoop, Snap, Flatpak und Apt installieren.
```sh ```sh
# Auf Mac via Homebrew # Auf Mac via Homebrew
@ -122,11 +123,11 @@ Oder einer Versionskontrolle deiner Wahl
### Unterstützung ❤️ ### Unterstützung ❤️
Wuff! Wenn du dieses Projekt magst, klick den ⭐ Button !! Wuff! Wenn du dieses Projekt magst, klick auf den ⭐ Button !!
### Teile Erfahrungsberichte 📣 ### Teile Erfahrungsberichte 📣
Wenn Bruno dir und in deinen Teams bei der Arbeit geholfen hat, vergiss bitte nicht, deine [Erfahrungsberichte auf unserer GitHub-Diskussion](https://github.com/usebruno/bruno/discussions/343) zu teilen. Wenn Bruno dir und in deinem Team bei der Arbeit geholfen hat, vergiss bitte nicht, deine [Erfahrungsberichte in unserer GitHub-Diskussion](https://github.com/usebruno/bruno/discussions/343) zu teilen.
### Bereitstellung in neuen Paket-Managern ### Bereitstellung in neuen Paket-Managern

View File

@ -27,6 +27,8 @@
| [正體中文](./readme_zhtw.md) | [正體中文](./readme_zhtw.md)
| [العربية](./readme_ar.md) | [العربية](./readme_ar.md)
| [日本語](./readme_ja.md) | [日本語](./readme_ja.md)
| [ქართული](./readme_ka.md)
Bruno es un cliente de APIs nuevo e innovador, creado con el objetivo de revolucionar el panorama actual representado por Postman y otras herramientas similares. Bruno es un cliente de APIs nuevo e innovador, creado con el objetivo de revolucionar el panorama actual representado por Postman y otras herramientas similares.
Bruno almacena tus colecciones directamente en una carpeta de tu sistema de archivos. Usamos un lenguaje de marcado de texto plano, llamado Bru, para guardar información sobre las peticiones a tus APIs. Bruno almacena tus colecciones directamente en una carpeta de tu sistema de archivos. Usamos un lenguaje de marcado de texto plano, llamado Bru, para guardar información sobre las peticiones a tus APIs.

View File

@ -27,6 +27,7 @@
| [正體中文](./readme_zhtw.md) | [正體中文](./readme_zhtw.md)
| [العربية](./readme_ar.md) | [العربية](./readme_ar.md)
| [日本語](./readme_ja.md) | [日本語](./readme_ja.md)
| [ქართული](./readme_ka.md)
Bruno est un nouveau client API, innovant, qui a pour but de révolutionner le _statu quo_ que représentent Postman et les autres outils. Bruno est un nouveau client API, innovant, qui a pour but de révolutionner le _statu quo_ que représentent Postman et les autres outils.

View File

@ -27,6 +27,7 @@
| [正體中文](./readme_zhtw.md) | [正體中文](./readme_zhtw.md)
| [العربية](./readme_ar.md) | [العربية](./readme_ar.md)
| [日本語](./readme_ja.md) | [日本語](./readme_ja.md)
| [ქართული](./readme_ka.md)
Bruno è un nuovo ed innovativo API client, mirato a rivoluzionare lo status quo rappresentato da Postman e strumenti simili disponibili. Bruno è un nuovo ed innovativo API client, mirato a rivoluzionare lo status quo rappresentato da Postman e strumenti simili disponibili.

View File

@ -27,6 +27,7 @@
| [正體中文](./readme_zhtw.md) | [正體中文](./readme_zhtw.md)
| [العربية](./readme_ar.md) | [العربية](./readme_ar.md)
| **日本語** | **日本語**
| [ქართული](./readme_ka.md)
Bruno は革新的な API クライアントです。Postman を代表する API クライアントツールの現状に一石を投じることを目指しています。 Bruno は革新的な API クライアントです。Postman を代表する API クライアントツールの現状に一石を投じることを目指しています。

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

@ -0,0 +1,176 @@
<br />
<img src="../../assets/images/logo-transparent.png" width="80"/>
### ბრუნო - ღია წყაროების IDE API-ების შესწავლისა და ტესტირებისათვის.
[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%bruno)
[![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
[![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse)
[![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno)
[![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com)
[![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads)
[English](../../readme.md)
| [Українська](./readme_ua.md)
| [Русский](./readme_ru.md)
| [Türkçe](./readme_tr.md)
| [Deutsch](./readme_de.md)
| [Français](./readme_fr.md)
| [Português (BR)](./readme_pt_br.md)
| [한국어](./readme_kr.md)
| [বাংলা](./readme_bn.md)
| [Español](./readme_es.md)
| [Italiano](./readme_it.md)
| [Română](./readme_ro.md)
| [Polski](./readme_pl.md)
| [简体中文](./readme_cn.md)
| [正體中文](./readme_zhtw.md)
| [العربية](./readme_ar.md)
| [日本語](./readme_ja.md)
| **ქართული**
ბრუნო არის ახალი და ინოვაციური API კლიენტი, რომელიც მიზნად ისახავს პოსტმანისა და მსგავსი ინსტრუმენტების არსებული მდგომარეობის რევოლუციას.
ბრუნო თქვენი კოლექციების შენახვას უშუალოდ თქვენს ფაილური სისტემის ერთ-ერთ საქაღალოში ახდენს. ჩვენ ვხმარობთ უბრალო ტექსტურ მარკაპ ენის, Bru-ს, API მოთხოვნების შესახებ ინფორმაციის შენახვისთვის.
თქვენ შეგიძლიათ გამოიყენოთ Git ან ნებისმიერი ვერსიის კონტროლის სისტემა თქვენი API კოლექციების გასაზიარებლად.
ბრუნო მხოლოდ ოფლაინ რეჟიმში მუშაობს. ბრუნოში ღრუბლური სინქრონიზაციის დამატების გეგმები არ არის. ჩვენ ვაფასებთ თქვენი მონაცემების პრივატობას და creemos, რომ ისინი თქვენს მოწყობილობაში უნდა დარჩეს. წაიკითხეთ ჩვენი გრძელვადიანი ხედვა [აქ](https://github.com/usebruno/bruno/discussions/269)
[დამატებით ბრუნო](https://www.usebruno.com/downloads)
📢 შეიტყვეთ ჩვენი უახლესი საუბრის შესახებ India FOSS 3.0 კონფერენციაზე [აქ](https://www.youtube.com/watch?v=7bSMFpbcPiY)
![bruno](../../assets/images/landing-2.png) <br /><br />
### ოქროს გამოცემა ✨
მთავარი ფუნქციების უმეტესობა უფასოა და ღია წყაროა. ჩვენ ვცდილობთ ჰარმონიული ბალანსის დაცვას [ღია წყაროების პრინციპებსა და მდგრადობას შორის](https://github.com/usebruno/bruno/discussions/269)
თქვენ შეგიძლიათ შეიძინოთ [ოქროს გამოცემა](https://www.usebruno.com/pricing) ერთჯერადი გადახდით **19 დოლარად**! <br/>
### ინსტალაცია
ბრუნო ხელმისაწვდომია როგორც ბინარული ჩამოტვირთვა [ჩვენ的网站上](https://www.usebruno.com/downloads) Mac-ის, Windows-ისა და Linux-ისთვის.
თქვენ ასევე შეგიძლიათ დააინსტალიროთ ბრუნო პაკეტის მენეჯერების საშუალებით, როგორიცაა Homebrew, Chocolatey, Scoop, Snap, Flatpak და Apt.
```sh
# Mac-ზე Homebrew-ს საშუალებით
brew install bruno
# Windows-ზე Chocolatey-ს საშუალებით
choco install bruno
# Windows-ზე Scoop-ის საშუალებით
scoop bucket add extras
scoop install bruno
# Windows-ზე winget-ის საშუალებით
winget install Bruno.Bruno
# Linux-ზე Snap-ის საშუალებით
snap install bruno
# Linux-ზე Flatpak-ის საშუალებით
flatpak install com.usebruno.Bruno
# Linux-ზე Apt-ის საშუალებით
sudo mkdir -p /etc/apt/keyrings
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
echo "deb [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
```
### პლატფორმებს შორის მუშაობა 🖥️
![bruno](../../assets/images/run-anywhere.png) <br /><br />
### თანამშრომლობა Git-ის საშუალებით 👩‍💻🧑‍💻
ან ნებისმიერი ვერსიის კონტროლის სისტემის საშუალებით
![bruno](../../assets/images/version-control.png) <br /><br />
### სპონსორები
#### ოქროს სპონსორები
<img src="../../assets/images/sponsors/samagata.png" width="150"/>
#### ვერცხლის სპონსორები
<img src="../../assets/images/sponsors/commit-company.png" width="70"/>
#### ბრინჯის სპონსორები
<a href="https://zuplo.link/bruno">
<img src="../../assets/images/sponsors/zuplo.png" width="120"/>
</a>
### მნიშვნელოვანი ბმულები 📌
- [ჩვენი გრძელვადიანი ხედვა](https://github.com/usebruno/bruno/discussions/269)
- [გეგმა](https://github.com/usebruno/bruno/discussions/384)
- [დოკუმენტაცია](https://docs.usebruno.com)
- [Stack Overflow](https://stackoverflow.com/questions/tagged/bruno)
- [ვებსაიტი](https://www.usebruno.com)
- [ფასები](https://www.usebruno.com/pricing)
- [დამატება](https://www.usebruno.com/downloads)
- [GitHub სპონსორები](https://github.com/sponsors/helloanoop).
### ვიტრინა 🎥
- [მოწონებები](https://github.com/usebruno/bruno/discussions/343)
- [მეცნიერების ჰაბი](https://github.com/usebruno/bruno/discussions/386)
- [Scriptmania](https://github.com/usebruno/bruno/discussions/385)
### მხარდაჭერა ❤️
თუ გიყვართ ბრუნო და გინდათ მხარი დაუჭიროთ ჩვენს ღია წყაროების მუშაობას, გაითვალისწინეთ ჩვენი დახმარება [GitHub სპონსორების საშუალებით](https://github.com/sponsors/helloanoop).
### გააზიარეთ მოწმობები 📣
თუ ბრუნო დაგეხმარათ თქვენს სამუშაოში და გუნდებში, გთხოვთ, არ დაგავიწყდეთ ჩვენი [მოწონებების გაზიარება ჩვენს GitHub განხილვაში](https://github.com/usebruno/bruno/discussions/343)
### ახალი პაკეტის მენეჯერებში გამოქვეყნება
იხილეთ [აქ](../../publishing.md) მეტი ინფორმაციისათვის.
### დაინტერესდით 🌐
[𝕎 (Twitter)](https://twitter.com/use_bruno) <br />
[ვებსაიტი](https://www.usebruno.com) <br />
[Discord](https://discord.com/invite/KgcZUncpjq) <br />
[LinkedIn](https://www.linkedin.com/company/usebruno)
### სავაჭრო ნიშანი
**სახელი**
`ბრუნო` არის სავაჭრო ნიშანი, რომელსაც ფლობს [ანუპ მ. დ.](https://www.helloanoop.com/)
**ლოგო**
ლოგო არის [OpenMoji](https://openmoji.org/library/emoji-1F436/) სურათებიდან. ლიცენზია: CC [BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)
### თანამშრომლობა 👩‍💻🧑‍💻
მიხარია, რომ დაინტერესებული ხართ ბრუნოს გაუმჯობესებით. გთხოვთ, გადახედეთ [თანამშრომლობის სახელმძღვანელოს](../../contributing.md)
თუნდაც ვერ მოახერხოთ კოდის საშუალებით კონტრიბუცია, ნუ ინანებთ პრობლემების და ფუნქციის მოთხოვნების ჩაწერას, რომლებიც უნდა განხორციელდეს თქვენი შემთხვევის გადასაჭრელად.
### ავტორები
<div align="center">
<a href="https://github.com/usebruno/bruno/graphs/contributors">
<img src="https://contrib.rocks/image?repo=usebruno/bruno" />
</a>
</div>
### ლიცენზია 📄
[MIT](../../license.md)

View File

@ -27,6 +27,7 @@
| [正體中文](./readme_zhtw.md) | [正體中文](./readme_zhtw.md)
| [العربية](./readme_ar.md) | [العربية](./readme_ar.md)
| [日本語](./readme_ja.md) | [日本語](./readme_ja.md)
| [ქართული](./readme_ka.md)
Bruno는 새롭고 혁신적인 API 클라이언트로, Postman과 유사한 툴들을 혁신하는 것을 목표로 합니다. Bruno는 새롭고 혁신적인 API 클라이언트로, Postman과 유사한 툴들을 혁신하는 것을 목표로 합니다.

View File

@ -27,6 +27,7 @@
| [正體中文](./readme_zhtw.md) | [正體中文](./readme_zhtw.md)
| [العربية](./readme_ar.md) | [العربية](./readme_ar.md)
| [日本語](./readme_ja.md) | [日本語](./readme_ja.md)
| [ქართული](./readme_ka.md)
Bruno to nowy i innowacyjny klient API, którego celem jest zrewolucjonizowanie status quo reprezentowanego przez narzędzia takie jak Postman. Bruno to nowy i innowacyjny klient API, którego celem jest zrewolucjonizowanie status quo reprezentowanego przez narzędzia takie jak Postman.

View File

@ -27,6 +27,7 @@
| [正體中文](./readme_zhtw.md) | [正體中文](./readme_zhtw.md)
| [العربية](./readme_ar.md) | [العربية](./readme_ar.md)
| [日本語](./readme_ja.md) | [日本語](./readme_ja.md)
| [ქართული](./readme_ka.md)
Bruno é um novo e inovador cliente de API, com o objetivo de revolucionar o status quo representado por ferramentas como o Postman e outras semelhantes. Bruno é um novo e inovador cliente de API, com o objetivo de revolucionar o status quo representado por ferramentas como o Postman e outras semelhantes.
@ -103,6 +104,12 @@ Ou qualquer sistema de controle de versão de sua escolha.
<img src="../../assets/images/sponsors/commit-company.png" width="70"/> <img src="../../assets/images/sponsors/commit-company.png" width="70"/>
#### Apoiadores Bronze
<a href="https://zuplo.link/bruno">
<img src="../../assets/images/sponsors/zuplo.png" width="120"/>
</a>
### Links Importantes 📌 ### Links Importantes 📌
- [Nossa Visão de Longo Prazo](https://github.com/usebruno/bruno/discussions/269) - [Nossa Visão de Longo Prazo](https://github.com/usebruno/bruno/discussions/269)

View File

@ -27,6 +27,7 @@
| [正體中文](./readme_zhtw.md) | [正體中文](./readme_zhtw.md)
| [العربية](./readme_ar.md) | [العربية](./readme_ar.md)
| [日本語](./readme_ja.md) | [日本語](./readme_ja.md)
| [ქართული](./readme_ka.md)
Bruno este un client API nou și inovativ, care vizează să revoluționeze status quo-ul reprezentat de Postman și alte instrumente similare. Bruno este un client API nou și inovativ, care vizează să revoluționeze status quo-ul reprezentat de Postman și alte instrumente similare.

View File

@ -27,6 +27,7 @@
| [正體中文](./readme_zhtw.md) | [正體中文](./readme_zhtw.md)
| [العربية](./readme_ar.md) | [العربية](./readme_ar.md)
| [日本語](./readme_ja.md) | [日本語](./readme_ja.md)
| [ქართული](./readme_ka.md)
Bruno - новый и инновационный клиент API, направленный на революцию в установившейся ситуации, представленной Postman и подобными инструментами. Bruno - новый и инновационный клиент API, направленный на революцию в установившейся ситуации, представленной Postman и подобными инструментами.

View File

@ -27,6 +27,7 @@
| [正體中文](./readme_zhtw.md) | [正體中文](./readme_zhtw.md)
| [العربية](./readme_ar.md) | [العربية](./readme_ar.md)
| [日本語](./readme_ja.md) | [日本語](./readme_ja.md)
| [ქართული](./readme_ka.md)
Bruno, Postman ve benzeri araçlar tarafından temsil edilen statükoda devrim yaratmayı amaçlayan yeni ve yenilikçi bir API istemcisidir. Bruno, Postman ve benzeri araçlar tarafından temsil edilen statükoda devrim yaratmayı amaçlayan yeni ve yenilikçi bir API istemcisidir.

View File

@ -27,6 +27,7 @@
| [正體中文](./readme_zhtw.md) | [正體中文](./readme_zhtw.md)
| [العربية](./readme_ar.md) | [العربية](./readme_ar.md)
| [日本語](./readme_ja.md) | [日本語](./readme_ja.md)
| [ქართული](./readme_ka.md)
Bruno це новий та іноваційний API клієнт, націлений на революційну зміну статус кво, запровадженого інструментами на кшталт Postman. Bruno це новий та іноваційний API клієнт, націлений на революційну зміну статус кво, запровадженого інструментами на кшталт Postman.

View File

@ -27,6 +27,7 @@
| **正體中文** | **正體中文**
| [العربية](./readme_ar.md) | [العربية](./readme_ar.md)
| [日本語](./readme_ja.md) | [日本語](./readme_ja.md)
| [ქართული](./readme_ka.md)
Bruno 是一個全新且有創新性的 API 用戶端,目的在徹底改變以 Postman 和其他類似工具的現況。 Bruno 是一個全新且有創新性的 API 用戶端,目的在徹底改變以 Postman 和其他類似工具的現況。

22615
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -20,14 +20,17 @@
"@jest/globals": "^29.2.0", "@jest/globals": "^29.2.0",
"@playwright/test": "^1.27.1", "@playwright/test": "^1.27.1",
"@types/jest": "^29.5.11", "@types/jest": "^29.5.11",
"concurrently": "^8.2.2",
"fs-extra": "^11.1.1", "fs-extra": "^11.1.1",
"husky": "^8.0.3", "husky": "^8.0.3",
"jest": "^29.2.0", "jest": "^29.2.0",
"pretty-quick": "^3.1.3", "pretty-quick": "^3.1.3",
"randomstring": "^1.2.2", "randomstring": "^1.2.2",
"rimraf": "^6.0.1",
"ts-jest": "^29.0.5" "ts-jest": "^29.0.5"
}, },
"scripts": { "scripts": {
"dev": "concurrently --kill-others \"npm run dev:web\" \"npm run dev:electron\"",
"dev:web": "npm run dev --workspace=packages/bruno-app", "dev:web": "npm run dev --workspace=packages/bruno-app",
"build:web": "npm run build --workspace=packages/bruno-app", "build:web": "npm run build --workspace=packages/bruno-app",
"prettier:web": "npm run prettier --workspace=packages/bruno-app", "prettier:web": "npm run prettier --workspace=packages/bruno-app",
@ -47,9 +50,8 @@
"test:prettier:web": "npm run test:prettier --workspace=packages/bruno-app", "test:prettier:web": "npm run test:prettier --workspace=packages/bruno-app",
"prepare": "husky install" "prepare": "husky install"
}, },
"overrides": { "overrides": {
"rollup": "3.2.5" "rollup":"3.29.4"
}, },
"dependencies": { "dependencies": {
"json-bigint": "^1.0.0", "json-bigint": "^1.0.0",

View File

@ -12,6 +12,7 @@
"prettier": "prettier --write \"./src/**/*.{js,jsx,json,ts,tsx}\"" "prettier": "prettier --write \"./src/**/*.{js,jsx,json,ts,tsx}\""
}, },
"dependencies": { "dependencies": {
"@fontsource/inter": "^5.0.15",
"@fortawesome/fontawesome-svg-core": "^1.2.36", "@fortawesome/fontawesome-svg-core": "^1.2.36",
"@fortawesome/free-solid-svg-icons": "^5.15.4", "@fortawesome/free-solid-svg-icons": "^5.15.4",
"@fortawesome/react-fontawesome": "^0.1.16", "@fortawesome/react-fontawesome": "^0.1.16",
@ -35,7 +36,8 @@
"graphiql": "^1.5.9", "graphiql": "^1.5.9",
"graphql": "^16.6.0", "graphql": "^16.6.0",
"graphql-request": "^3.7.0", "graphql-request": "^3.7.0",
"httpsnippet": "^3.0.1", "httpsnippet": "^3.0.6",
"i18next": "^23.14.0",
"idb": "^7.0.0", "idb": "^7.0.0",
"immer": "^9.0.15", "immer": "^9.0.15",
"jsesc": "^3.0.2", "jsesc": "^3.0.2",
@ -47,6 +49,7 @@
"know-your-http-well": "^0.5.0", "know-your-http-well": "^0.5.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"markdown-it": "^13.0.2", "markdown-it": "^13.0.2",
"markdown-it-replace-link": "^1.2.0",
"mousetrap": "^1.6.5", "mousetrap": "^1.6.5",
"nanoid": "3.3.4", "nanoid": "3.3.4",
"next": "12.3.3", "next": "12.3.3",
@ -64,6 +67,7 @@
"react-dom": "18.2.0", "react-dom": "18.2.0",
"react-github-btn": "^1.4.0", "react-github-btn": "^1.4.0",
"react-hot-toast": "^2.4.0", "react-hot-toast": "^2.4.0",
"react-i18next": "^15.0.1",
"react-inspector": "^6.0.2", "react-inspector": "^6.0.2",
"react-pdf": "^7.5.1", "react-pdf": "^7.5.1",
"react-redux": "^7.2.6", "react-redux": "^7.2.6",

View File

@ -5,7 +5,9 @@ const StyledWrapper = styled.div`
background: ${(props) => props.theme.codemirror.bg}; background: ${(props) => props.theme.codemirror.bg};
border: solid 1px ${(props) => props.theme.codemirror.border}; border: solid 1px ${(props) => props.theme.codemirror.border};
font-family: ${(props) => (props.font ? props.font : 'default')}; font-family: ${(props) => (props.font ? props.font : 'default')};
font-size: ${(props) => (props.fontSize ? `${props.fontSize}px` : 'inherit')};
line-break: anywhere; line-break: anywhere;
flex: 1 1 0;
} }
.CodeMirror-overlayscroll-horizontal div, .CodeMirror-overlayscroll-horizontal div,
@ -13,6 +15,24 @@ const StyledWrapper = styled.div`
background: #d2d7db; background: #d2d7db;
} }
.CodeMirror-dialog {
overflow: visible;
}
#search-results-count {
display: inline-block;
position: absolute;
top: calc(100% + 1px);
right: 0;
border-width: 0 0 1px 1px;
border-style: solid;
border-color: ${(props) => props.theme.codemirror.border};
padding: 0.1em 0.8em;
background-color: ${(props) => props.theme.codemirror.bg};
color: rgb(102, 102, 102);
white-space: nowrap;
}
textarea.cm-editor { textarea.cm-editor {
position: relative; position: relative;
} }

View File

@ -16,6 +16,7 @@ import stripJsonComments from 'strip-json-comments';
let CodeMirror; let CodeMirror;
const SERVER_RENDERED = typeof navigator === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true; const SERVER_RENDERED = typeof navigator === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
const TAB_SIZE = 2;
if (!SERVER_RENDERED) { if (!SERVER_RENDERED) {
CodeMirror = require('codemirror'); CodeMirror = require('codemirror');
@ -58,11 +59,17 @@ if (!SERVER_RENDERED) {
'bru.cwd()', 'bru.cwd()',
'bru.getEnvName(key)', 'bru.getEnvName(key)',
'bru.getProcessEnv(key)', 'bru.getProcessEnv(key)',
'bru.hasEnvVar(key)',
'bru.getEnvVar(key)', 'bru.getEnvVar(key)',
'bru.setEnvVar(key,value)', 'bru.setEnvVar(key,value)',
'bru.hasVar(key)',
'bru.getVar(key)', 'bru.getVar(key)',
'bru.setVar(key,value)', 'bru.setVar(key,value)',
'bru.setNextRequest(requestName)' 'bru.deleteVar(key)',
'bru.setNextRequest(requestName)',
'req.disableParsingResponseJson()',
'bru.getRequestVar(key)',
'bru.sleep(ms)'
]; ];
CodeMirror.registerHelper('hint', 'brunoJS', (editor, options) => { CodeMirror.registerHelper('hint', 'brunoJS', (editor, options) => {
const cursor = editor.getCursor(); const cursor = editor.getCursor();
@ -105,6 +112,7 @@ export default class CodeEditor extends React.Component {
// unnecessary updates during the update lifecycle. // unnecessary updates during the update lifecycle.
this.cachedValue = props.value || ''; this.cachedValue = props.value || '';
this.variables = {}; this.variables = {};
this.searchResultsCountElementId = 'search-results-count';
this.lintOptions = { this.lintOptions = {
esversion: 11, esversion: 11,
@ -118,7 +126,7 @@ export default class CodeEditor extends React.Component {
value: this.props.value || '', value: this.props.value || '',
lineNumbers: true, lineNumbers: true,
lineWrapping: true, lineWrapping: true,
tabSize: 2, tabSize: TAB_SIZE,
mode: this.props.mode || 'application/ld+json', mode: this.props.mode || 'application/ld+json',
keyMap: 'sublime', keyMap: 'sublime',
autoCloseBrackets: true, autoCloseBrackets: true,
@ -151,8 +159,16 @@ export default class CodeEditor extends React.Component {
this.props.onSave(); this.props.onSave();
} }
}, },
'Cmd-F': 'findPersistent', 'Cmd-F': (cm) => {
'Ctrl-F': 'findPersistent', cm.execCommand('findPersistent');
this._bindSearchHandler();
this._appendSearchResultsCount();
},
'Ctrl-F': (cm) => {
cm.execCommand('findPersistent');
this._bindSearchHandler();
this._appendSearchResultsCount();
},
'Cmd-H': 'replace', 'Cmd-H': 'replace',
'Ctrl-H': 'replace', 'Ctrl-H': 'replace',
Tab: function (cm) { Tab: function (cm) {
@ -166,7 +182,33 @@ export default class CodeEditor extends React.Component {
'Ctrl-Y': 'foldAll', 'Ctrl-Y': 'foldAll',
'Cmd-Y': 'foldAll', 'Cmd-Y': 'foldAll',
'Ctrl-I': 'unfoldAll', 'Ctrl-I': 'unfoldAll',
'Cmd-I': 'unfoldAll' 'Cmd-I': 'unfoldAll',
'Cmd-/': (cm) => {
// comment/uncomment every selected line(s)
const selections = cm.listSelections();
selections.forEach((range) => {
for (let i = range.from().line; i <= range.to().line; i++) {
const selectedLine = cm.getLine(i);
// if commented line, remove comment
if (selectedLine.trim().startsWith('//')) {
cm.replaceRange(
selectedLine.replace(/^(\s*)\/\/\s?/, '$1'),
{ line: i, ch: 0 },
{ line: i, ch: selectedLine.length }
);
continue;
}
// otherwise add comment
cm.replaceRange(
selectedLine.search(/\S|$/) >= TAB_SIZE
? ' '.repeat(TAB_SIZE) + '// ' + selectedLine.trim()
: '// ' + selectedLine,
{ line: i, ch: 0 },
{ line: i, ch: selectedLine.length }
);
}
});
}
}, },
foldOptions: { foldOptions: {
widget: (from, to) => { widget: (from, to) => {
@ -278,6 +320,8 @@ export default class CodeEditor extends React.Component {
this.editor.off('change', this._onEdit); this.editor.off('change', this._onEdit);
this.editor = null; this.editor = null;
} }
this._unbindSearchHandler();
} }
render() { render() {
@ -286,9 +330,10 @@ export default class CodeEditor extends React.Component {
} }
return ( return (
<StyledWrapper <StyledWrapper
className="h-full w-full" className="h-full w-full flex flex-col relative"
aria-label="Code Editor" aria-label="Code Editor"
font={this.props.font} font={this.props.font}
fontSize={this.props.fontSize}
ref={(node) => { ref={(node) => {
this._node = node; this._node = node;
}} }}
@ -314,4 +359,62 @@ export default class CodeEditor extends React.Component {
} }
} }
}; };
/**
* Bind handler to search input to count number of search results
*/
_bindSearchHandler = () => {
const searchInput = document.querySelector('.CodeMirror-search-field');
if (searchInput) {
searchInput.addEventListener('input', this._countSearchResults);
}
};
/**
* Unbind handler to search input to count number of search results
*/
_unbindSearchHandler = () => {
const searchInput = document.querySelector('.CodeMirror-search-field');
if (searchInput) {
searchInput.removeEventListener('input', this._countSearchResults);
}
};
/**
* Append search results count to search dialog
*/
_appendSearchResultsCount = () => {
const dialog = document.querySelector('.CodeMirror-dialog.CodeMirror-dialog-top');
if (dialog) {
const searchResultsCount = document.createElement('span');
searchResultsCount.id = this.searchResultsCountElementId;
dialog.appendChild(searchResultsCount);
this._countSearchResults();
}
};
/**
* Count search results and update state
*/
_countSearchResults = () => {
let count = 0;
const searchInput = document.querySelector('.CodeMirror-search-field');
if (searchInput && searchInput.value.length > 0) {
const text = new RegExp(searchInput.value, 'gi');
const matches = this.editor.getValue().match(text);
count = matches ? matches.length : 0;
}
const searchResultsCountElement = document.querySelector(`#${this.searchResultsCountElementId}`);
if (searchResultsCountElement) {
searchResultsCountElement.innerText = `${count} results`;
}
};
} }

View File

@ -138,6 +138,7 @@ const AwsV4Auth = ({ collection }) => {
onSave={handleSave} onSave={handleSave}
onChange={(val) => handleSecretAccessKeyChange(val)} onChange={(val) => handleSecretAccessKeyChange(val)}
collection={collection} collection={collection}
isSecret={true}
/> />
</div> </div>

View File

@ -62,6 +62,7 @@ const BasicAuth = ({ collection }) => {
onSave={handleSave} onSave={handleSave}
onChange={(val) => handlePasswordChange(val)} onChange={(val) => handlePasswordChange(val)}
collection={collection} collection={collection}
isSecret={true}
/> />
</div> </div>
</StyledWrapper> </StyledWrapper>

View File

@ -37,6 +37,7 @@ const BearerAuth = ({ collection }) => {
onSave={handleSave} onSave={handleSave}
onChange={(val) => handleTokenChange(val)} onChange={(val) => handleTokenChange(val)}
collection={collection} collection={collection}
isSecret={true}
/> />
</div> </div>
</StyledWrapper> </StyledWrapper>

View File

@ -62,6 +62,7 @@ const DigestAuth = ({ collection }) => {
onSave={handleSave} onSave={handleSave}
onChange={(val) => handlePasswordChange(val)} onChange={(val) => handlePasswordChange(val)}
collection={collection} collection={collection}
isSecret={true}
/> />
</div> </div>
</StyledWrapper> </StyledWrapper>

View File

@ -78,7 +78,7 @@ const OAuth2AuthorizationCode = ({ collection }) => {
return ( return (
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col"> <StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
{inputsConfig.map((input) => { {inputsConfig.map((input) => {
const { key, label } = input; const { key, label, isSecret } = input;
return ( return (
<div className="flex flex-col w-full gap-1" key={`input-${key}`}> <div className="flex flex-col w-full gap-1" key={`input-${key}`}>
<label className="block font-medium">{label}</label> <label className="block font-medium">{label}</label>
@ -90,6 +90,7 @@ const OAuth2AuthorizationCode = ({ collection }) => {
onChange={(val) => handleChange(key, val)} onChange={(val) => handleChange(key, val)}
onRun={handleRun} onRun={handleRun}
collection={collection} collection={collection}
isSecret={isSecret}
/> />
</div> </div>
</div> </div>

View File

@ -17,7 +17,8 @@ const inputsConfig = [
}, },
{ {
key: 'clientSecret', key: 'clientSecret',
label: 'Client Secret' label: 'Client Secret',
isSecret: true
}, },
{ {
key: 'scope', key: 'scope',

View File

@ -42,7 +42,7 @@ const OAuth2ClientCredentials = ({ collection }) => {
return ( return (
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col"> <StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
{inputsConfig.map((input) => { {inputsConfig.map((input) => {
const { key, label } = input; const { key, label, isSecret } = input;
return ( return (
<div className="flex flex-col w-full gap-1" key={`input-${key}`}> <div className="flex flex-col w-full gap-1" key={`input-${key}`}>
<label className="block font-medium">{label}</label> <label className="block font-medium">{label}</label>
@ -54,6 +54,7 @@ const OAuth2ClientCredentials = ({ collection }) => {
onChange={(val) => handleChange(key, val)} onChange={(val) => handleChange(key, val)}
onRun={handleRun} onRun={handleRun}
collection={collection} collection={collection}
isSecret={isSecret}
/> />
</div> </div>
</div> </div>

View File

@ -9,7 +9,8 @@ const inputsConfig = [
}, },
{ {
key: 'clientSecret', key: 'clientSecret',
label: 'Client Secret' label: 'Client Secret',
isSecret: true
}, },
{ {
key: 'scope', key: 'scope',

View File

@ -44,7 +44,7 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
return ( return (
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col"> <StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
{inputsConfig.map((input) => { {inputsConfig.map((input) => {
const { key, label } = input; const { key, label, isSecret } = input;
return ( return (
<div className="flex flex-col w-full gap-1" key={`input-${key}`}> <div className="flex flex-col w-full gap-1" key={`input-${key}`}>
<label className="block font-medium">{label}</label> <label className="block font-medium">{label}</label>
@ -56,6 +56,7 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
onChange={(val) => handleChange(key, val)} onChange={(val) => handleChange(key, val)}
onRun={handleRun} onRun={handleRun}
collection={collection} collection={collection}
isSecret={isSecret}
/> />
</div> </div>
</div> </div>

View File

@ -17,7 +17,8 @@ const inputsConfig = [
}, },
{ {
key: 'clientSecret', key: 'clientSecret',
label: 'Client Secret' label: 'Client Secret',
isSecret: true
}, },
{ {
key: 'scope', key: 'scope',

View File

@ -10,8 +10,9 @@ import StyledWrapper from './StyledWrapper';
import { useRef } from 'react'; import { useRef } from 'react';
import path from 'path'; import path from 'path';
import slash from 'utils/common/slash'; import slash from 'utils/common/slash';
import { isWindowsOS } from 'utils/common/platform';
const ClientCertSettings = ({ clientCertConfig, onUpdate, onRemove }) => { const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
const certFilePathInputRef = useRef(); const certFilePathInputRef = useRef();
const keyFilePathInputRef = useRef(); const keyFilePathInputRef = useRef();
const pfxFilePathInputRef = useRef(); const pfxFilePathInputRef = useRef();
@ -67,7 +68,15 @@ const ClientCertSettings = ({ clientCertConfig, onUpdate, onRemove }) => {
}); });
const getFile = (e) => { const getFile = (e) => {
e.files?.[0]?.path && formik.setFieldValue(e.name, e.files?.[0]?.path); if (e.files?.[0]?.path) {
let relativePath;
if (isWindowsOS()) {
relativePath = slash(path.win32.relative(root, e.files[0].path));
} else {
relativePath = path.posix.relative(root, e.files[0].path);
}
formik.setFieldValue(e.name, relativePath);
}
}; };
const resetFileInputFields = () => { const resetFileInputFields = () => {
@ -102,10 +111,14 @@ const ClientCertSettings = ({ clientCertConfig, onUpdate, onRemove }) => {
: clientCertConfig.map((clientCert) => ( : clientCertConfig.map((clientCert) => (
<li key={uuid()} className="flex items-center available-certificates p-2 rounded-lg mb-2"> <li key={uuid()} className="flex items-center available-certificates p-2 rounded-lg mb-2">
<div className="flex items-center w-full justify-between"> <div className="flex items-center w-full justify-between">
<div className="flex items-center"> <div className="flex w-full items-center">
<IconWorld className="mr-2" size={18} strokeWidth={1.5} /> <IconWorld className="mr-2" size={18} strokeWidth={1.5} />
{clientCert.domain} {clientCert.domain}
</div> </div>
<div className="flex w-full items-center">
<IconCertificate className="mr-2 flex-shrink-0" size={18} strokeWidth={1.5} />
{clientCert.type === 'cert' ? clientCert.certFilePath : clientCert.pfxFilePath}
</div>
<button onClick={() => onRemove(clientCert)} className="remove-certificate ml-2"> <button onClick={() => onRemove(clientCert)} className="remove-certificate ml-2">
<IconTrash size={18} strokeWidth={1.5} /> <IconTrash size={18} strokeWidth={1.5} />
</button> </button>

View File

@ -46,9 +46,10 @@ const Docs = ({ collection }) => {
onSave={onSave} onSave={onSave}
mode="application/text" mode="application/text"
font={get(preferences, 'font.codeFont', 'default')} font={get(preferences, 'font.codeFont', 'default')}
fontSize={get(preferences, 'font.codeFontSize')}
/> />
) : ( ) : (
<Markdown onDoubleClick={toggleViewMode} content={docs} /> <Markdown collectionPath={collection.pathname} onDoubleClick={toggleViewMode} content={docs} />
)} )}
</StyledWrapper> </StyledWrapper>
); );

View File

@ -13,6 +13,7 @@ import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/acti
import SingleLineEditor from 'components/SingleLineEditor'; import SingleLineEditor from 'components/SingleLineEditor';
import StyledWrapper from './StyledWrapper'; import StyledWrapper from './StyledWrapper';
import { headers as StandardHTTPHeaders } from 'know-your-http-well'; import { headers as StandardHTTPHeaders } from 'know-your-http-well';
import { MimeTypes } from 'utils/codemirror/autocompleteConstants';
const headerAutoCompleteList = StandardHTTPHeaders.map((e) => e.header); const headerAutoCompleteList = StandardHTTPHeaders.map((e) => e.header);
const Headers = ({ collection }) => { const Headers = ({ collection }) => {
@ -117,6 +118,7 @@ const Headers = ({ collection }) => {
) )
} }
collection={collection} collection={collection}
autocomplete={MimeTypes}
/> />
</td> </td>
<td> <td>

View File

@ -74,6 +74,7 @@ const PresetsSettings = ({ collection }) => {
id="request-url" id="request-url"
type="text" type="text"
name="requestUrl" name="requestUrl"
placeholder='Request URL'
className="block textbox" className="block textbox"
autoComplete="off" autoComplete="off"
autoCorrect="off" autoCorrect="off"

View File

@ -1,6 +1,6 @@
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { useFormik } from 'formik'; import { useFormik } from 'formik';
import Tooltip from 'components/Tooltip'; import InfoTip from 'components/InfoTip';
import StyledWrapper from './StyledWrapper'; import StyledWrapper from './StyledWrapper';
import * as Yup from 'yup'; import * as Yup from 'yup';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
@ -104,7 +104,7 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
<div className="mb-3 flex items-center"> <div className="mb-3 flex items-center">
<label className="settings-label flex items-center" htmlFor="enabled"> <label className="settings-label flex items-center" htmlFor="enabled">
Config Config
<Tooltip <InfoTip
text={` text={`
<div> <div>
<ul> <ul>
@ -114,7 +114,7 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
</ul> </ul>
</div> </div>
`} `}
tooltipId="request-var" infotipId="request-var"
/> />
</label> </label>
<div className="flex items-center"> <div className="flex items-center">

View File

@ -52,6 +52,7 @@ const Script = ({ collection }) => {
mode="javascript" mode="javascript"
onSave={handleSave} onSave={handleSave}
font={get(preferences, 'font.codeFont', 'default')} font={get(preferences, 'font.codeFont', 'default')}
fontSize={get(preferences, 'font.codeFontSize')}
/> />
</div> </div>
<div className="flex-1 mt-6"> <div className="flex-1 mt-6">
@ -64,6 +65,7 @@ const Script = ({ collection }) => {
mode="javascript" mode="javascript"
onSave={handleSave} onSave={handleSave}
font={get(preferences, 'font.codeFont', 'default')} font={get(preferences, 'font.codeFont', 'default')}
fontSize={get(preferences, 'font.codeFontSize')}
/> />
</div> </div>

View File

@ -36,6 +36,7 @@ const Tests = ({ collection }) => {
mode="javascript" mode="javascript"
onSave={handleSave} onSave={handleSave}
font={get(preferences, 'font.codeFont', 'default')} font={get(preferences, 'font.codeFont', 'default')}
fontSize={get(preferences, 'font.codeFontSize')}
/> />
<div className="mt-6"> <div className="mt-6">

View File

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

View File

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

View File

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

View File

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

View File

@ -16,6 +16,7 @@ import Docs from './Docs';
import Presets from './Presets'; import Presets from './Presets';
import Info from './Info'; import Info from './Info';
import StyledWrapper from './StyledWrapper'; import StyledWrapper from './StyledWrapper';
import Vars from './Vars/index';
const CollectionSettings = ({ collection }) => { const CollectionSettings = ({ collection }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
@ -77,6 +78,9 @@ const CollectionSettings = ({ collection }) => {
case 'headers': { case 'headers': {
return <Headers collection={collection} />; return <Headers collection={collection} />;
} }
case 'vars': {
return <Vars collection={collection} />;
}
case 'auth': { case 'auth': {
return <Auth collection={collection} />; return <Auth collection={collection} />;
} }
@ -95,6 +99,7 @@ const CollectionSettings = ({ collection }) => {
case 'clientCert': { case 'clientCert': {
return ( return (
<ClientCertSettings <ClientCertSettings
root={collection.pathname}
clientCertConfig={clientCertConfig} clientCertConfig={clientCertConfig}
onUpdate={onClientCertSettingsUpdate} onUpdate={onClientCertSettingsUpdate}
onRemove={onClientCertSettingsRemove} onRemove={onClientCertSettingsRemove}
@ -122,6 +127,9 @@ const CollectionSettings = ({ collection }) => {
<div className={getTabClassname('headers')} role="tab" onClick={() => setTab('headers')}> <div className={getTabClassname('headers')} role="tab" onClick={() => setTab('headers')}>
Headers Headers
</div> </div>
<div className={getTabClassname('vars')} role="tab" onClick={() => setTab('vars')}>
Vars
</div>
<div className={getTabClassname('auth')} role="tab" onClick={() => setTab('auth')}> <div className={getTabClassname('auth')} role="tab" onClick={() => setTab('auth')}>
Auth Auth
</div> </div>

View File

@ -1,14 +1,6 @@
import styled from 'styled-components'; import styled from 'styled-components';
const StyledWrapper = styled.div` const StyledWrapper = styled.div`
div.CodeMirror {
/* todo: find a better way */
height: calc(100vh - 240px);
.CodeMirror-scroll {
padding-bottom: 0px;
}
}
.editing-mode { .editing-mode {
cursor: pointer; cursor: pointer;
color: ${(props) => props.theme.colors.text.yellow}; color: ${(props) => props.theme.colors.text.yellow};

View File

@ -37,8 +37,8 @@ const Documentation = ({ item, collection }) => {
} }
return ( return (
<StyledWrapper className="mt-1 h-full w-full relative"> <StyledWrapper className="flex flex-col gap-y-1 h-full w-full relative">
<div className="editing-mode mb-2" role="tab" onClick={toggleViewMode}> <div className="editing-mode" role="tab" onClick={toggleViewMode}>
{isEditing ? 'Preview' : 'Edit'} {isEditing ? 'Preview' : 'Edit'}
</div> </div>
@ -47,13 +47,14 @@ const Documentation = ({ item, collection }) => {
collection={collection} collection={collection}
theme={displayedTheme} theme={displayedTheme}
font={get(preferences, 'font.codeFont', 'default')} font={get(preferences, 'font.codeFont', 'default')}
fontSize={get(preferences, 'font.codeFontSize')}
value={docs || ''} value={docs || ''}
onEdit={onEdit} onEdit={onEdit}
onSave={onSave} onSave={onSave}
mode="application/text" mode="application/text"
/> />
) : ( ) : (
<Markdown onDoubleClick={toggleViewMode} content={docs} /> <Markdown collectionPath={collection.pathname} onDoubleClick={toggleViewMode} content={docs} />
)} )}
</StyledWrapper> </StyledWrapper>
); );

View File

@ -40,10 +40,15 @@ const Wrapper = styled.div`
color: ${(props) => props.theme.dropdown.iconColor}; color: ${(props) => props.theme.dropdown.iconColor};
} }
&:hover { &:hover:not(:disabled) {
background-color: ${(props) => props.theme.dropdown.hoverBg}; background-color: ${(props) => props.theme.dropdown.hoverBg};
} }
&:disabled {
cursor: not-allowed;
color: gray;
}
&.border-top { &.border-top {
border-top: solid 1px ${(props) => props.theme.dropdown.separator}; border-top: solid 1px ${(props) => props.theme.dropdown.separator};
} }

View File

@ -1,11 +1,11 @@
import React, { useEffect, useRef } from 'react'; import React, { useEffect, useRef } from 'react';
import Portal from 'components/Portal';
import Modal from 'components/Modal';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import { useFormik } from 'formik'; import { useFormik } from 'formik';
import { addEnvironment } from 'providers/ReduxStore/slices/collections/actions'; import { addEnvironment } from 'providers/ReduxStore/slices/collections/actions';
import * as Yup from 'yup'; import * as Yup from 'yup';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import Portal from 'components/Portal';
import Modal from 'components/Modal';
const CreateEnvironment = ({ collection, onClose }) => { const CreateEnvironment = ({ collection, onClose }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
@ -27,7 +27,7 @@ const CreateEnvironment = ({ collection, onClose }) => {
toast.success('Environment created in collection'); toast.success('Environment created in collection');
onClose(); onClose();
}) })
.catch(() => toast.error('An error occurred while created the environment')); .catch(() => toast.error('An error occurred while creating the environment'));
} }
}); });
@ -55,19 +55,21 @@ const CreateEnvironment = ({ collection, onClose }) => {
<label htmlFor="name" className="block font-semibold"> <label htmlFor="name" className="block font-semibold">
Environment Name Environment Name
</label> </label>
<input <div className="flex items-center mt-2">
id="environment-name" <input
type="text" id="environment-name"
name="name" type="text"
ref={inputRef} name="name"
className="block textbox mt-2 w-full" ref={inputRef}
autoComplete="off" className="block textbox w-full"
autoCorrect="off" autoComplete="off"
autoCapitalize="off" autoCorrect="off"
spellCheck="false" autoCapitalize="off"
onChange={formik.handleChange} spellCheck="false"
value={formik.values.name || ''} onChange={formik.handleChange}
/> value={formik.values.name || ''}
/>
</div>
{formik.touched.name && formik.errors.name ? ( {formik.touched.name && formik.errors.name ? (
<div className="text-red-500">{formik.errors.name}</div> <div className="text-red-500">{formik.errors.name}</div>
) : null} ) : null}

View File

@ -5,7 +5,6 @@ import { useDispatch } from 'react-redux';
import SingleLineEditor from 'components/SingleLineEditor'; import SingleLineEditor from 'components/SingleLineEditor';
import StyledWrapper from './StyledWrapper'; import StyledWrapper from './StyledWrapper';
import { uuid } from 'utils/common'; import { uuid } from 'utils/common';
import { maskInputValue } from 'utils/collections';
import { useFormik } from 'formik'; import { useFormik } from 'formik';
import * as Yup from 'yup'; import * as Yup from 'yup';
import { variableNameRegex } from 'utils/common/regex'; import { variableNameRegex } from 'utils/common/regex';
@ -96,10 +95,10 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
<table> <table>
<thead> <thead>
<tr> <tr>
<td>Enabled</td> <td className="text-center">Enabled</td>
<td>Name</td> <td>Name</td>
<td>Value</td> <td>Value</td>
<td>Secret</td> <td className="text-center">Secret</td>
<td></td> <td></td>
</tr> </tr>
</thead> </thead>
@ -109,7 +108,7 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
<td className="text-center"> <td className="text-center">
<input <input
type="checkbox" type="checkbox"
className="mr-3 mousetrap" className="mousetrap"
name={`${index}.enabled`} name={`${index}.enabled`}
checked={variable.enabled} checked={variable.enabled}
onChange={formik.handleChange} onChange={formik.handleChange}
@ -130,23 +129,22 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
/> />
<ErrorMessage name={`${index}.name`} /> <ErrorMessage name={`${index}.name`} />
</td> </td>
<td> <td className="flex flex-row flex-nowrap">
{variable.secret ? ( <div className="overflow-hidden grow w-full relative">
<div className="overflow-hidden text-ellipsis">{maskInputValue(variable.value)}</div>
) : (
<SingleLineEditor <SingleLineEditor
theme={storedTheme} theme={storedTheme}
collection={collection} collection={collection}
name={`${index}.value`} name={`${index}.value`}
value={variable.value} value={variable.value}
isSecret={variable.secret}
onChange={(newValue) => formik.setFieldValue(`${index}.value`, newValue, true)} onChange={(newValue) => formik.setFieldValue(`${index}.value`, newValue, true)}
/> />
)} </div>
</td> </td>
<td> <td className="text-center">
<input <input
type="checkbox" type="checkbox"
className="mr-3 mousetrap" className="mousetrap"
name={`${index}.secret`} name={`${index}.secret`}
checked={variable.secret} checked={variable.secret}
onChange={formik.handleChange} onChange={formik.handleChange}

View File

@ -1,24 +1,38 @@
import React from 'react'; import React from 'react';
import Portal from 'components/Portal'; import Portal from 'components/Portal';
import Modal from 'components/Modal';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import importPostmanEnvironment from 'utils/importers/postman-environment'; import importPostmanEnvironment from 'utils/importers/postman-environment';
import { importEnvironment } from 'providers/ReduxStore/slices/collections/actions'; import { importEnvironment } from 'providers/ReduxStore/slices/collections/actions';
import { toastError } from 'utils/common/error'; import { toastError } from 'utils/common/error';
import Modal from 'components/Modal'; import { IconDatabaseImport } from '@tabler/icons';
const ImportEnvironment = ({ onClose, collection }) => { const ImportEnvironment = ({ collection, onClose }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const handleImportPostmanEnvironment = () => { const handleImportPostmanEnvironment = () => {
importPostmanEnvironment() importPostmanEnvironment()
.then((environment) => { .then((environments) => {
dispatch(importEnvironment(environment.name, environment.variables, collection.uid)) environments
.then(() => { .filter((env) =>
toast.success('Environment imported successfully'); env.name && env.name !== 'undefined'
onClose(); ? true
}) : () => {
.catch(() => toast.error('An error occurred while importing the environment')); toast.error('Failed to import environment: env has no name');
return false;
}
)
.map((environment) => {
dispatch(importEnvironment(environment.name, environment.variables, collection.uid))
.then(() => {
toast.success('Environment imported successfully');
})
.catch(() => toast.error('An error occurred while importing the environment'));
});
})
.then(() => {
onClose();
}) })
.catch((err) => toastError(err, 'Postman Import environment failed')); .catch((err) => toastError(err, 'Postman Import environment failed'));
}; };
@ -26,11 +40,14 @@ const ImportEnvironment = ({ onClose, collection }) => {
return ( return (
<Portal> <Portal>
<Modal size="sm" title="Import Environment" hideFooter={true} handleConfirm={onClose} handleCancel={onClose}> <Modal size="sm" title="Import Environment" hideFooter={true} handleConfirm={onClose} handleCancel={onClose}>
<div> <button
<div className="text-link hover:underline cursor-pointer" onClick={handleImportPostmanEnvironment}> type="button"
Postman Environment onClick={handleImportPostmanEnvironment}
</div> className="flex justify-center flex-col items-center w-full dark:bg-zinc-700 rounded-lg border-2 border-dashed border-zinc-300 dark:border-zinc-400 p-12 text-center hover:border-zinc-400 focus:outline-none focus:ring-2 focus:ring-amber-500 focus:ring-offset-2"
</div> >
<IconDatabaseImport size={64} />
<span className="mt-2 block text-sm font-semibold">Import your Postman environments</span>
</button>
</Modal> </Modal>
</Portal> </Portal>
); );

View File

@ -4,45 +4,61 @@ import CreateEnvironment from './CreateEnvironment';
import EnvironmentList from './EnvironmentList'; import EnvironmentList from './EnvironmentList';
import StyledWrapper from './StyledWrapper'; import StyledWrapper from './StyledWrapper';
import ImportEnvironment from './ImportEnvironment'; import ImportEnvironment from './ImportEnvironment';
import { IconFileAlert } from '@tabler/icons';
export const SharedButton = ({ children, className, onClick }) => {
return (
<button
type="button"
onClick={onClick}
className={`rounded bg-transparent px-2.5 py-2 w-fit text-xs font-semibold text-zinc-900 dark:text-zinc-50 shadow-sm ring-1 ring-inset ring-zinc-300 dark:ring-zinc-500 hover:bg-gray-50 dark:hover:bg-zinc-700
${className}`}
>
{children}
</button>
);
};
const DefaultTab = ({ setTab }) => {
return (
<div className="text-center items-center flex flex-col">
<IconFileAlert size={64} strokeWidth={1} />
<span className="font-semibold mt-2">No environments found</span>
<span className="font-extralight mt-2 text-zinc-500 dark:text-zinc-400">
Get started by using the following buttons :
</span>
<div className="flex items-center justify-center mt-6">
<SharedButton onClick={() => setTab('create')}>
<span>Create Environment</span>
</SharedButton>
<span className="mx-4">Or</span>
<SharedButton onClick={() => setTab('import')}>
<span>Import Environment</span>
</SharedButton>
</div>
</div>
);
};
const EnvironmentSettings = ({ collection, onClose }) => { const EnvironmentSettings = ({ collection, onClose }) => {
const [isModified, setIsModified] = useState(false); const [isModified, setIsModified] = useState(false);
const { environments } = collection; const { environments } = collection;
const [openCreateModal, setOpenCreateModal] = useState(false);
const [openImportModal, setOpenImportModal] = useState(false);
const [selectedEnvironment, setSelectedEnvironment] = useState(null); const [selectedEnvironment, setSelectedEnvironment] = useState(null);
const [tab, setTab] = useState('default');
if (!environments || !environments.length) { if (!environments || !environments.length) {
return ( return (
<StyledWrapper> <StyledWrapper>
<Modal <Modal size="md" title="Environments" handleCancel={onClose} hideCancel={true} hideFooter={true}>
size="md" {tab === 'create' ? (
title="Environments" <CreateEnvironment collection={collection} onClose={() => setTab('default')} />
confirmText={'Close'} ) : tab === 'import' ? (
handleConfirm={onClose} <ImportEnvironment collection={collection} onClose={() => setTab('default')} />
handleCancel={onClose} ) : (
hideCancel={true} <></>
> )}
{openCreateModal && <CreateEnvironment collection={collection} onClose={() => setOpenCreateModal(false)} />} <DefaultTab setTab={setTab} />
{openImportModal && <ImportEnvironment collection={collection} onClose={() => setOpenImportModal(false)} />}
<div className="text-center flex flex-col">
<p>No environments found!</p>
<button
className="btn-create-environment text-link pr-2 py-3 mt-2 select-none"
onClick={() => setOpenCreateModal(true)}
>
<span>Create Environment</span>
</button>
<span>Or</span>
<button
className="btn-import-environment text-link pl-2 pr-2 py-3 select-none"
onClick={() => setOpenImportModal(true)}
>
<span>Import Environment</span>
</button>
</div>
</Modal> </Modal>
</StyledWrapper> </StyledWrapper>
); );

View File

@ -42,7 +42,7 @@ const FilePickerEditor = ({ value, onChange, collection }) => {
}; };
const clear = () => { const clear = () => {
onChange(''); onChange([]);
}; };
const renderButtonText = (filenames) => { const renderButtonText = (filenames) => {

View File

@ -116,6 +116,7 @@ const Headers = ({ collection, folder }) => {
) )
} }
collection={collection} collection={collection}
item={folder}
/> />
</td> </td>
<td> <td>

View File

@ -44,8 +44,8 @@ const Script = ({ collection, folder }) => {
<div className="text-xs mb-4 text-muted"> <div className="text-xs mb-4 text-muted">
Pre and post-request scripts that will run before and after any request inside this folder is sent. Pre and post-request scripts that will run before and after any request inside this folder is sent.
</div> </div>
<div className="flex-1 mt-2"> <div className="flex flex-col flex-1 mt-2 gap-y-2">
<div className="mb-1 title text-xs">Pre Request</div> <div className="title text-xs">Pre Request</div>
<CodeEditor <CodeEditor
collection={collection} collection={collection}
value={requestScript || ''} value={requestScript || ''}
@ -54,10 +54,11 @@ const Script = ({ collection, folder }) => {
mode="javascript" mode="javascript"
onSave={handleSave} onSave={handleSave}
font={get(preferences, 'font.codeFont', 'default')} font={get(preferences, 'font.codeFont', 'default')}
fontSize={get(preferences, 'font.codeFontSize')}
/> />
</div> </div>
<div className="flex-1 mt-6"> <div className="flex flex-col flex-1 mt-2 gap-y-2">
<div className="mt-1 mb-1 title text-xs">Post Response</div> <div className="title text-xs">Post Response</div>
<CodeEditor <CodeEditor
collection={collection} collection={collection}
value={responseScript || ''} value={responseScript || ''}
@ -66,6 +67,7 @@ const Script = ({ collection, folder }) => {
mode="javascript" mode="javascript"
onSave={handleSave} onSave={handleSave}
font={get(preferences, 'font.codeFont', 'default')} font={get(preferences, 'font.codeFont', 'default')}
fontSize={get(preferences, 'font.codeFontSize')}
/> />
</div> </div>

View File

@ -37,6 +37,7 @@ const Tests = ({ collection, folder }) => {
mode="javascript" mode="javascript"
onSave={handleSave} onSave={handleSave}
font={get(preferences, 'font.codeFont', 'default')} font={get(preferences, 'font.codeFont', 'default')}
fontSize={get(preferences, 'font.codeFontSize')}
/> />
<div className="mt-6"> <div className="mt-6">

View File

@ -5,7 +5,7 @@ import { useDispatch } from 'react-redux';
import { useTheme } from 'providers/Theme'; import { useTheme } from 'providers/Theme';
import { saveFolderRoot } from 'providers/ReduxStore/slices/collections/actions'; import { saveFolderRoot } from 'providers/ReduxStore/slices/collections/actions';
import SingleLineEditor from 'components/SingleLineEditor'; import SingleLineEditor from 'components/SingleLineEditor';
import Tooltip from 'components/Tooltip'; import InfoTip from 'components/InfoTip';
import StyledWrapper from './StyledWrapper'; import StyledWrapper from './StyledWrapper';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import { variableNameRegex } from 'utils/common/regex'; import { variableNameRegex } from 'utils/common/regex';
@ -82,14 +82,14 @@ const VarsTable = ({ folder, collection, vars, varType }) => {
<td> <td>
<div className="flex items-center"> <div className="flex items-center">
<span>Value</span> <span>Value</span>
<Tooltip text="You can write any valid JS Template Literal here" tooltipId="request-var" /> <InfoTip text="You can write any valid JS Template Literal here" infotipId="request-var" />
</div> </div>
</td> </td>
) : ( ) : (
<td> <td>
<div className="flex items-center"> <div className="flex items-center">
<span>Expr</span> <span>Expr</span>
<Tooltip text="You can write any valid JS expression here" tooltipId="response-var" /> <InfoTip text="You can write any valid JS expression here" infotipId="response-var" />
</div> </div>
</td> </td>
)} )}
@ -130,6 +130,7 @@ const VarsTable = ({ folder, collection, vars, varType }) => {
) )
} }
collection={collection} collection={collection}
item={folder}
/> />
</td> </td>
<td> <td>

View File

@ -12,15 +12,15 @@ const FolderSettings = ({ collection, folder }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
let tab = 'headers'; let tab = 'headers';
const { folderLevelSettingsSelectedTab } = collection; const { folderLevelSettingsSelectedTab } = collection;
if (folderLevelSettingsSelectedTab?.[folder.uid]) { if (folderLevelSettingsSelectedTab?.[folder?.uid]) {
tab = folderLevelSettingsSelectedTab[folder.uid]; tab = folderLevelSettingsSelectedTab[folder?.uid];
} }
const setTab = (tab) => { const setTab = (tab) => {
dispatch( dispatch(
updatedFolderSettingsSelectedTab({ updatedFolderSettingsSelectedTab({
collectionUid: collection.uid, collectionUid: collection?.uid,
folderUid: folder.uid, folderUid: folder?.uid,
tab tab
}) })
); );
@ -50,7 +50,7 @@ const FolderSettings = ({ collection, folder }) => {
}; };
return ( return (
<StyledWrapper> <StyledWrapper className="flex flex-col h-full">
<div className="flex flex-col h-full relative px-4 py-4"> <div className="flex flex-col h-full relative px-4 py-4">
<div className="flex flex-wrap items-center tabs" role="tablist"> <div className="flex flex-wrap items-center tabs" role="tablist">
<div className={getTabClassname('headers')} role="tab" onClick={() => setTab('headers')}> <div className={getTabClassname('headers')} role="tab" onClick={() => setTab('headers')}>

View File

@ -0,0 +1,16 @@
import React from 'react';
const DotIcon = ({ width }) => {
return (
<svg xmlns="http://www.w3.org/2000/svg" width={width} height={width}
viewBox="0 0 24 24" strokeWidth="1.5"
stroke="currentColor" fill="none" strokeLinecap="round" strokeLinejoin="round"
className='inline-block'
>
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M12 7a5 5 0 1 1 -4.995 5.217l-.005 -.217l.005 -.217a5 5 0 0 1 4.995 -4.783z" stroke-width="0" fill="currentColor" />
</svg>
);
};
export default DotIcon;

View File

@ -1,26 +1,25 @@
import React from 'react'; import React from 'react';
import { Tooltip as ReactTooltip } from 'react-tooltip'; import { Tooltip as ReactInfoTip } from 'react-tooltip';
const Tooltip = ({ text, tooltipId }) => { const InfoTip = ({ text, infotipId }) => {
return ( return (
<> <>
<svg <svg
tabIndex="-1" tabIndex="-1"
id={tooltipId} id={infotipId}
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
width="14" width="14"
height="14" height="14"
fill="currentColor" fill="currentColor"
className="inline-block ml-2 cursor-pointer" className="inline-block ml-2 cursor-pointer"
viewBox="0 0 16 16" viewBox="0 0 16 16"
style={{ marginTop: 1 }}
> >
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z" /> <path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z" />
<path d="M5.255 5.786a.237.237 0 0 0 .241.247h.825c.138 0 .248-.113.266-.25.09-.656.54-1.134 1.342-1.134.686 0 1.314.343 1.314 1.168 0 .635-.374.927-.965 1.371-.673.489-1.206 1.06-1.168 1.987l.003.217a.25.25 0 0 0 .25.246h.811a.25.25 0 0 0 .25-.25v-.105c0-.718.273-.927 1.01-1.486.609-.463 1.244-.977 1.244-2.056 0-1.511-1.276-2.241-2.673-2.241-1.267 0-2.655.59-2.75 2.286zm1.557 5.763c0 .533.425.927 1.01.927.609 0 1.028-.394 1.028-.927 0-.552-.42-.94-1.029-.94-.584 0-1.009.388-1.009.94z" /> <path d="M5.255 5.786a.237.237 0 0 0 .241.247h.825c.138 0 .248-.113.266-.25.09-.656.54-1.134 1.342-1.134.686 0 1.314.343 1.314 1.168 0 .635-.374.927-.965 1.371-.673.489-1.206 1.06-1.168 1.987l.003.217a.25.25 0 0 0 .25.246h.811a.25.25 0 0 0 .25-.25v-.105c0-.718.273-.927 1.01-1.486.609-.463 1.244-.977 1.244-2.056 0-1.511-1.276-2.241-2.673-2.241-1.267 0-2.655.59-2.75 2.286zm1.557 5.763c0 .533.425.927 1.01.927.609 0 1.028-.394 1.028-.927 0-.552-.42-.94-1.029-.94-.584 0-1.009.388-1.009.94z" />
</svg> </svg>
<ReactTooltip anchorId={tooltipId} html={text} /> <ReactInfoTip anchorId={infotipId} html={text} />
</> </>
); );
}; };
export default Tooltip; export default InfoTip;

View File

@ -2,7 +2,6 @@ import styled from 'styled-components';
const StyledMarkdownBodyWrapper = styled.div` const StyledMarkdownBodyWrapper = styled.div`
background: transparent; background: transparent;
height: inherit;
.markdown-body { .markdown-body {
background: transparent; background: transparent;
overflow-y: auto; overflow-y: auto;
@ -70,6 +69,7 @@ const StyledMarkdownBodyWrapper = styled.div`
pre { pre {
background: ${(props) => props.theme.sidebar.bg}; background: ${(props) => props.theme.sidebar.bg};
color: ${(props) => props.theme.text};
} }
table { table {

View File

@ -1,15 +1,35 @@
import MarkdownIt from 'markdown-it'; import MarkdownIt from 'markdown-it';
import * as MarkdownItReplaceLink from 'markdown-it-replace-link';
import StyledWrapper from './StyledWrapper'; import StyledWrapper from './StyledWrapper';
import * as React from 'react'; import React from 'react';
const md = new MarkdownIt(); const Markdown = ({ collectionPath, onDoubleClick, content }) => {
const markdownItOptions = {
replaceLink: function (link, env) {
return link.replace(/^\./, collectionPath);
}
};
const handleOnClick = (event) => {
const target = event.target;
if (target.tagName === 'A') {
event.preventDefault();
const href = target.getAttribute('href');
if (href) {
window.open(href, '_blank');
return;
}
}
};
const Markdown = ({ onDoubleClick, content }) => {
const handleOnDoubleClick = (event) => { const handleOnDoubleClick = (event) => {
if (event?.detail === 2) { if (event.detail === 2) {
onDoubleClick(); onDoubleClick();
} }
}; };
const md = new MarkdownIt(markdownItOptions).use(MarkdownItReplaceLink);
const htmlFromMarkdown = md.render(content || ''); const htmlFromMarkdown = md.render(content || '');
return ( return (
@ -17,7 +37,8 @@ const Markdown = ({ onDoubleClick, content }) => {
<div <div
className="markdown-body" className="markdown-body"
dangerouslySetInnerHTML={{ __html: htmlFromMarkdown }} dangerouslySetInnerHTML={{ __html: htmlFromMarkdown }}
onClick={handleOnDoubleClick} onClick={handleOnClick}
onDoubleClick={handleOnDoubleClick}
/> />
</StyledWrapper> </StyledWrapper>
); );

View File

@ -1,10 +1,10 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import StyledWrapper from './StyledWrapper'; import StyledWrapper from './StyledWrapper';
const ModalHeader = ({ title, handleCancel, customHeader }) => ( const ModalHeader = ({ title, handleCancel, customHeader, hideClose }) => (
<div className="bruno-modal-header"> <div className="bruno-modal-header">
{customHeader ? customHeader : <>{title ? <div className="bruno-modal-header-title">{title}</div> : null}</>} {customHeader ? customHeader : <>{title ? <div className="bruno-modal-header-title">{title}</div> : null}</>}
{handleCancel ? ( {handleCancel && !hideClose ? (
<div className="close cursor-pointer" onClick={handleCancel ? () => handleCancel() : null}> <div className="close cursor-pointer" onClick={handleCancel ? () => handleCancel() : null}>
× ×
</div> </div>
@ -63,6 +63,7 @@ const Modal = ({
confirmDisabled, confirmDisabled,
hideCancel, hideCancel,
hideFooter, hideFooter,
hideClose,
disableCloseOnOutsideClick, disableCloseOnOutsideClick,
disableEscapeKey, disableEscapeKey,
onClick, onClick,
@ -100,7 +101,12 @@ const Modal = ({
return ( return (
<StyledWrapper className={classes} onClick={onClick ? (e) => onClick(e) : null}> <StyledWrapper className={classes} onClick={onClick ? (e) => onClick(e) : null}>
<div className={`bruno-modal-card modal-${size}`}> <div className={`bruno-modal-card modal-${size}`}>
<ModalHeader title={title} handleCancel={() => closeModal({ type: 'icon' })} customHeader={customHeader} /> <ModalHeader
title={title}
hideClose={hideClose}
handleCancel={() => closeModal({ type: 'icon' })}
customHeader={customHeader}
/>
<ModalContent>{children}</ModalContent> <ModalContent>{children}</ModalContent>
<ModalFooter <ModalFooter
confirmText={confirmText} confirmText={confirmText}

View File

@ -23,7 +23,7 @@ const StyledWrapper = styled.div`
overflow: hidden !important; overflow: hidden !important;
${'' /* padding-bottom: 50px !important; */} ${'' /* padding-bottom: 50px !important; */}
position: relative; position: relative;
display: contents; display: block;
margin: 0px; margin: 0px;
padding: 0px; padding: 0px;
} }

View File

@ -24,6 +24,8 @@ class MultiLineEditor extends Component {
componentDidMount() { componentDidMount() {
// Initialize CodeMirror as a single line editor // Initialize CodeMirror as a single line editor
/** @type {import("codemirror").Editor} */ /** @type {import("codemirror").Editor} */
const variables = getAllVariables(this.props.collection, this.props.item);
this.editor = CodeMirror(this.editorRef.current, { this.editor = CodeMirror(this.editorRef.current, {
lineWrapping: false, lineWrapping: false,
lineNumbers: false, lineNumbers: false,
@ -31,7 +33,7 @@ class MultiLineEditor extends Component {
placeholder: this.props.placeholder, placeholder: this.props.placeholder,
mode: 'brunovariables', mode: 'brunovariables',
brunoVarInfo: { brunoVarInfo: {
variables: getAllVariables(this.props.collection) variables
}, },
scrollbarStyle: null, scrollbarStyle: null,
tabindex: 0, tabindex: 0,
@ -86,7 +88,7 @@ class MultiLineEditor extends Component {
} }
this.editor.setValue(String(this.props.value) || ''); this.editor.setValue(String(this.props.value) || '');
this.editor.on('change', this._onEdit); this.editor.on('change', this._onEdit);
this.addOverlay(); this.addOverlay(variables);
} }
_onEdit = () => { _onEdit = () => {
@ -104,10 +106,10 @@ class MultiLineEditor extends Component {
// event loop. // event loop.
this.ignoreChangeEvent = true; this.ignoreChangeEvent = true;
let variables = getAllVariables(this.props.collection); let variables = getAllVariables(this.props.collection, this.props.item);
if (!isEqual(variables, this.variables)) { if (!isEqual(variables, this.variables)) {
this.editor.options.brunoVarInfo.variables = variables; this.editor.options.brunoVarInfo.variables = variables;
this.addOverlay(); this.addOverlay(variables);
} }
if (this.props.theme !== prevProps.theme && this.editor) { if (this.props.theme !== prevProps.theme && this.editor) {
this.editor.setOption('theme', this.props.theme === 'dark' ? 'monokai' : 'default'); this.editor.setOption('theme', this.props.theme === 'dark' ? 'monokai' : 'default');
@ -126,10 +128,8 @@ class MultiLineEditor extends Component {
this.editor.getWrapperElement().remove(); this.editor.getWrapperElement().remove();
} }
addOverlay = () => { addOverlay = (variables) => {
let variables = getAllVariables(this.props.collection);
this.variables = variables; this.variables = variables;
defineCodeMirrorBrunoVariablesMode(variables, 'text/plain'); defineCodeMirrorBrunoVariablesMode(variables, 'text/plain');
this.editor.setOption('mode', 'brunovariables'); this.editor.setOption('mode', 'brunovariables');
}; };

View File

@ -10,6 +10,8 @@ import {
} from 'providers/ReduxStore/slices/notifications'; } from 'providers/ReduxStore/slices/notifications';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { humanizeDate, relativeDate } from 'utils/common'; import { humanizeDate, relativeDate } from 'utils/common';
import ToolHint from 'components/ToolHint';
import { useTheme } from 'providers/Theme';
const PAGE_SIZE = 5; const PAGE_SIZE = 5;
@ -20,6 +22,7 @@ const Notifications = () => {
const [showNotificationsModal, setShowNotificationsModal] = useState(false); const [showNotificationsModal, setShowNotificationsModal] = useState(false);
const [selectedNotification, setSelectedNotification] = useState(null); const [selectedNotification, setSelectedNotification] = useState(null);
const [pageNumber, setPageNumber] = useState(1); const [pageNumber, setPageNumber] = useState(1);
const { storedTheme } = useTheme();
const notificationsStartIndex = (pageNumber - 1) * PAGE_SIZE; const notificationsStartIndex = (pageNumber - 1) * PAGE_SIZE;
const notificationsEndIndex = pageNumber * PAGE_SIZE; const notificationsEndIndex = pageNumber * PAGE_SIZE;
@ -85,21 +88,22 @@ const Notifications = () => {
return ( return (
<StyledWrapper> <StyledWrapper>
<a <a
title="Notifications"
className="relative cursor-pointer" className="relative cursor-pointer"
onClick={() => { onClick={() => {
dispatch(fetchNotifications()); dispatch(fetchNotifications());
setShowNotificationsModal(true); setShowNotificationsModal(true);
}} }}
> >
<IconBell <ToolHint text="Notifications" toolhintId="Notifications" offset={8} >
size={18} <IconBell
strokeWidth={1.5} size={18}
className={`mr-2 hover:text-gray-700 ${unreadNotifications?.length > 0 ? 'bell' : ''}`} strokeWidth={1.5}
/> className={`mr-2 ${unreadNotifications?.length > 0 ? 'bell' : ''}`}
{unreadNotifications.length > 0 && ( />
<span className="notification-count text-xs">{unreadNotifications.length}</span> {unreadNotifications.length > 0 && (
)} <span className="notification-count text-xs">{unreadNotifications.length}</span>
)}
</ToolHint>
</a> </a>
{showNotificationsModal && ( {showNotificationsModal && (
@ -129,9 +133,8 @@ const Notifications = () => {
{notifications?.slice(notificationsStartIndex, notificationsEndIndex)?.map((notification) => ( {notifications?.slice(notificationsStartIndex, notificationsEndIndex)?.map((notification) => (
<li <li
key={notification.id} key={notification.id}
className={`p-4 flex flex-col justify-center ${ className={`p-4 flex flex-col justify-center ${selectedNotification?.id == notification.id ? 'active' : notification.read ? 'read' : ''
selectedNotification?.id == notification.id ? 'active' : notification.read ? 'read' : '' }`}
}`}
onClick={handleNotificationItemClick(notification)} onClick={handleNotificationItemClick(notification)}
> >
<div className="notification-title w-full">{notification?.title}</div> <div className="notification-title w-full">{notification?.title}</div>
@ -141,9 +144,8 @@ const Notifications = () => {
</ul> </ul>
<div className="w-full pagination flex flex-row gap-4 justify-center p-2 items-center text-xs"> <div className="w-full pagination flex flex-row gap-4 justify-center p-2 items-center text-xs">
<button <button
className={`pl-2 pr-2 py-3 select-none ${ className={`pl-2 pr-2 py-3 select-none ${pageNumber <= 1 ? 'opacity-50' : 'text-link cursor-pointer hover:underline'
pageNumber <= 1 ? 'opacity-50' : 'text-link cursor-pointer hover:underline' }`}
}`}
onClick={handlePrev} onClick={handlePrev}
> >
{'Prev'} {'Prev'}
@ -159,9 +161,8 @@ const Notifications = () => {
</div> </div>
</div> </div>
<button <button
className={`pl-2 pr-2 py-3 select-none ${ className={`pl-2 pr-2 py-3 select-none ${pageNumber == totalPages ? 'opacity-50' : 'text-link cursor-pointer hover:underline'
pageNumber == totalPages ? 'opacity-50' : 'text-link cursor-pointer hover:underline' }`}
}`}
onClick={handleNext} onClick={handleNext}
> >
{'Next'} {'Next'}

View File

@ -9,17 +9,25 @@ const Font = ({ close }) => {
const preferences = useSelector((state) => state.app.preferences); const preferences = useSelector((state) => state.app.preferences);
const [codeFont, setCodeFont] = useState(get(preferences, 'font.codeFont', 'default')); const [codeFont, setCodeFont] = useState(get(preferences, 'font.codeFont', 'default'));
const [codeFontSize, setCodeFontSize] = useState(get(preferences, 'font.codeFontSize', '14'));
const handleInputChange = (event) => { const handleCodeFontChange = (event) => {
setCodeFont(event.target.value); setCodeFont(event.target.value);
}; };
const handleCodeFontSizeChange = (event) => {
// Restrict to min/max value
const clampedSize = Math.max(1, Math.min(event.target.value, 32));
setCodeFontSize(clampedSize);
};
const handleSave = () => { const handleSave = () => {
dispatch( dispatch(
savePreferences({ savePreferences({
...preferences, ...preferences,
font: { font: {
codeFont codeFont,
codeFontSize
} }
}) })
).then(() => { ).then(() => {
@ -29,17 +37,33 @@ const Font = ({ close }) => {
return ( return (
<StyledWrapper> <StyledWrapper>
<label className="block font-medium">Code Editor Font</label> <div className="flex flex-row gap-2 w-full">
<input <div className="w-4/5">
type="text" <label className="block font-medium">Code Editor Font</label>
className="block textbox mt-2 w-full" <input
autoComplete="off" type="text"
autoCorrect="off" className="block textbox mt-2 w-full"
autoCapitalize="off" autoComplete="off"
spellCheck="false" autoCorrect="off"
onChange={handleInputChange} autoCapitalize="off"
defaultValue={codeFont} spellCheck="false"
/> onChange={handleCodeFontChange}
defaultValue={codeFont}
/>
</div>
<div className="w-1/5">
<label className="block font-medium">Font Size</label>
<input
type="number"
className="block textbox mt-2 w-full"
autoComplete="off"
autoCorrect="off"
inputMode="numeric"
onChange={handleCodeFontSizeChange}
defaultValue={codeFontSize}
/>
</div>
</div>
<div className="mt-10"> <div className="mt-10">
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}> <button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>

View File

@ -2,7 +2,7 @@ import styled from 'styled-components';
const StyledWrapper = styled.div` const StyledWrapper = styled.div`
.settings-label { .settings-label {
width: 80px; width: 100px;
} }
.textbox { .textbox {
@ -20,6 +20,12 @@ const StyledWrapper = styled.div`
outline: none !important; outline: none !important;
} }
} }
.system-proxy-settings {
label {
color: ${(props) => props.theme.colors.text.yellow};
}
}
`; `;
export default StyledWrapper; export default StyledWrapper;

View File

@ -11,14 +11,17 @@ import { useState } from 'react';
const ProxySettings = ({ close }) => { const ProxySettings = ({ close }) => {
const preferences = useSelector((state) => state.app.preferences); const preferences = useSelector((state) => state.app.preferences);
const systemProxyEnvVariables = useSelector((state) => state.app.systemProxyEnvVariables);
const { http_proxy, https_proxy, no_proxy } = systemProxyEnvVariables || {};
const dispatch = useDispatch(); const dispatch = useDispatch();
console.log(preferences);
const proxySchema = Yup.object({ const proxySchema = Yup.object({
enabled: Yup.boolean(), mode: Yup.string().oneOf(['off', 'on', 'system']),
protocol: Yup.string().required().oneOf(['http', 'https', 'socks4', 'socks5']), protocol: Yup.string().required().oneOf(['http', 'https', 'socks4', 'socks5']),
hostname: Yup.string() hostname: Yup.string()
.when('enabled', { .when('enabled', {
is: true, is: 'on',
then: (hostname) => hostname.required('Specify the hostname for your proxy.'), then: (hostname) => hostname.required('Specify the hostname for your proxy.'),
otherwise: (hostname) => hostname.nullable() otherwise: (hostname) => hostname.nullable()
}) })
@ -31,7 +34,7 @@ const ProxySettings = ({ close }) => {
.transform((_, val) => (val ? Number(val) : null)), .transform((_, val) => (val ? Number(val) : null)),
auth: Yup.object() auth: Yup.object()
.when('enabled', { .when('enabled', {
is: true, is: 'on',
then: Yup.object({ then: Yup.object({
enabled: Yup.boolean(), enabled: Yup.boolean(),
username: Yup.string() username: Yup.string()
@ -54,7 +57,7 @@ const ProxySettings = ({ close }) => {
const formik = useFormik({ const formik = useFormik({
initialValues: { initialValues: {
enabled: preferences.proxy.enabled || false, mode: preferences.proxy.mode,
protocol: preferences.proxy.protocol || 'http', protocol: preferences.proxy.protocol || 'http',
hostname: preferences.proxy.hostname || '', hostname: preferences.proxy.hostname || '',
port: preferences.proxy.port || 0, port: preferences.proxy.port || 0,
@ -94,7 +97,7 @@ const ProxySettings = ({ close }) => {
useEffect(() => { useEffect(() => {
formik.setValues({ formik.setValues({
enabled: preferences.proxy.enabled || false, mode: preferences.proxy.mode,
protocol: preferences.proxy.protocol || 'http', protocol: preferences.proxy.protocol || 'http',
hostname: preferences.proxy.hostname || '', hostname: preferences.proxy.hostname || '',
port: preferences.proxy.port || '', port: preferences.proxy.port || '',
@ -109,188 +112,256 @@ const ProxySettings = ({ close }) => {
return ( return (
<StyledWrapper> <StyledWrapper>
<h1 className="font-medium mb-3">Global Proxy Settings</h1>
<form className="bruno-form" onSubmit={formik.handleSubmit}> <form className="bruno-form" onSubmit={formik.handleSubmit}>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="enabled">
Enabled
</label>
<input type="checkbox" name="enabled" checked={formik.values.enabled} onChange={formik.handleChange} />
</div>
<div className="mb-3 flex items-center"> <div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="protocol"> <label className="settings-label" htmlFor="protocol">
Protocol Mode
</label> </label>
<div className="flex items-center"> <div className="flex items-center">
<label className="flex items-center"> <label className="flex items-center cursor-pointer">
<input <input
type="radio" type="radio"
name="protocol" name="mode"
value="http" value="false"
checked={formik.values.protocol === 'http'} checked={formik.values.mode === 'off'}
onChange={formik.handleChange} onChange={(e) => {
className="mr-1" formik.setFieldValue('mode', 'off');
}}
className="mr-1 cursor-pointer"
/> />
HTTP Off
</label> </label>
<label className="flex items-center ml-4"> <label className="flex items-center ml-4 cursor-pointer">
<input <input
type="radio" type="radio"
name="protocol" name="mode"
value="https" value="true"
checked={formik.values.protocol === 'https'} checked={formik.values.mode === 'on'}
onChange={formik.handleChange} onChange={(e) => {
className="mr-1" formik.setFieldValue('mode', 'on');
}}
className="mr-1 cursor-pointer"
/> />
HTTPS On
</label> </label>
<label className="flex items-center ml-4"> <label className="flex items-center ml-4 cursor-pointer">
<input <input
type="radio" type="radio"
name="protocol" name="mode"
value="socks4" value="system"
checked={formik.values.protocol === 'socks4'} checked={formik.values.mode === 'system'}
onChange={formik.handleChange} onChange={formik.handleChange}
className="mr-1" className="mr-1 cursor-pointer"
/> />
SOCKS4 System Proxy
</label>
<label className="flex items-center ml-4">
<input
type="radio"
name="protocol"
value="socks5"
checked={formik.values.protocol === 'socks5'}
onChange={formik.handleChange}
className="mr-1"
/>
SOCKS5
</label> </label>
</div> </div>
</div> </div>
{formik?.values?.mode === 'system' ? (
<div className="mb-3 flex items-center"> <div className="mb-3 pt-1 text-muted system-proxy-settings">
<label className="settings-label" htmlFor="hostname"> <small>
Hostname Below values are sourced from your system environment variables and cannot be directly updated in Bruno.<br/>
</label> Please refer to your OS documentation to change these values.
<input </small>
id="hostname" <div className="flex flex-col justify-start items-start pt-2">
type="text" <div className="mb-1 flex items-center">
name="hostname" <label className="settings-label" htmlFor="http_proxy">
className="block textbox" http_proxy
autoComplete="off" </label>
autoCorrect="off" <div className="opacity-80">{http_proxy || '-'}</div>
autoCapitalize="off" </div>
spellCheck="false" <div className="mb-1 flex items-center">
onChange={formik.handleChange} <label className="settings-label" htmlFor="https_proxy">
value={formik.values.hostname || ''} https_proxy
/> </label>
{formik.touched.hostname && formik.errors.hostname ? ( <div className="opacity-80">{https_proxy || '-'}</div>
<div className="ml-3 text-red-500">{formik.errors.hostname}</div> </div>
) : null} <div className="mb-1 flex items-center">
</div> <label className="settings-label" htmlFor="no_proxy">
<div className="mb-3 flex items-center"> no_proxy
<label className="settings-label" htmlFor="port"> </label>
Port <div className="opacity-80">{no_proxy || '-'}</div>
</label> </div>
<input </div>
id="port"
type="number"
name="port"
className="block textbox"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={formik.handleChange}
value={formik.values.port}
/>
{formik.touched.port && formik.errors.port ? (
<div className="ml-3 text-red-500">{formik.errors.port}</div>
) : null}
</div>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="auth.enabled">
Auth
</label>
<input
type="checkbox"
name="auth.enabled"
checked={formik.values.auth.enabled}
onChange={formik.handleChange}
/>
</div>
<div>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="auth.username">
Username
</label>
<input
id="auth.username"
type="text"
name="auth.username"
className="block textbox"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={formik.values.auth.username}
onChange={formik.handleChange}
/>
{formik.touched.auth?.username && formik.errors.auth?.username ? (
<div className="ml-3 text-red-500">{formik.errors.auth.username}</div>
) : null}
</div> </div>
<div className="mb-3 flex items-center"> ) : null}
<label className="settings-label" htmlFor="auth.password"> {formik?.values?.mode === 'on' ? (
Password <>
</label> <div className="mb-3 flex items-center">
<div className="textbox flex flex-row items-center w-[13.2rem] h-[2.25rem] relative"> <label className="settings-label" htmlFor="protocol">
Protocol
</label>
<div className="flex items-center">
<label className="flex items-center">
<input
type="radio"
name="protocol"
value="http"
checked={formik.values.protocol === 'http'}
onChange={formik.handleChange}
className="mr-1"
/>
HTTP
</label>
<label className="flex items-center ml-4">
<input
type="radio"
name="protocol"
value="https"
checked={formik.values.protocol === 'https'}
onChange={formik.handleChange}
className="mr-1"
/>
HTTPS
</label>
<label className="flex items-center ml-4">
<input
type="radio"
name="protocol"
value="socks4"
checked={formik.values.protocol === 'socks4'}
onChange={formik.handleChange}
className="mr-1"
/>
SOCKS4
</label>
<label className="flex items-center ml-4">
<input
type="radio"
name="protocol"
value="socks5"
checked={formik.values.protocol === 'socks5'}
onChange={formik.handleChange}
className="mr-1"
/>
SOCKS5
</label>
</div>
</div>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="hostname">
Hostname
</label>
<input <input
id="auth.password" id="hostname"
type={passwordVisible ? `text` : 'password'} type="text"
name="auth.password" name="hostname"
className="outline-none w-[10.5rem] bg-transparent" className="block textbox"
autoComplete="off" autoComplete="off"
autoCorrect="off" autoCorrect="off"
autoCapitalize="off" autoCapitalize="off"
spellCheck="false" spellCheck="false"
value={formik.values.auth.password} onChange={formik.handleChange}
value={formik.values.hostname || ''}
/>
{formik.touched.hostname && formik.errors.hostname ? (
<div className="ml-3 text-red-500">{formik.errors.hostname}</div>
) : null}
</div>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="port">
Port
</label>
<input
id="port"
type="number"
name="port"
className="block textbox"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={formik.handleChange}
value={formik.values.port}
/>
{formik.touched.port && formik.errors.port ? (
<div className="ml-3 text-red-500">{formik.errors.port}</div>
) : null}
</div>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="auth.enabled">
Auth
</label>
<input
type="checkbox"
name="auth.enabled"
checked={formik.values.auth.enabled}
onChange={formik.handleChange} onChange={formik.handleChange}
/> />
<button
type="button"
className="btn btn-sm absolute right-0"
onClick={() => setPasswordVisible(!passwordVisible)}
>
{passwordVisible ? <IconEyeOff size={18} strokeWidth={2} /> : <IconEye size={18} strokeWidth={2} />}
</button>
</div> </div>
{formik.touched.auth?.password && formik.errors.auth?.password ? ( <div>
<div className="ml-3 text-red-500">{formik.errors.auth.password}</div> <div className="mb-3 flex items-center">
) : null} <label className="settings-label" htmlFor="auth.username">
</div> Username
</div> </label>
<div className="mb-3 flex items-center"> <input
<label className="settings-label" htmlFor="bypassProxy"> id="auth.username"
Proxy Bypass type="text"
</label> name="auth.username"
<input className="block textbox"
id="bypassProxy" autoComplete="off"
type="text" autoCorrect="off"
name="bypassProxy" autoCapitalize="off"
className="block textbox" spellCheck="false"
autoComplete="off" value={formik.values.auth.username}
autoCorrect="off" onChange={formik.handleChange}
autoCapitalize="off" />
spellCheck="false" {formik.touched.auth?.username && formik.errors.auth?.username ? (
onChange={formik.handleChange} <div className="ml-3 text-red-500">{formik.errors.auth.username}</div>
value={formik.values.bypassProxy || ''} ) : null}
/> </div>
{formik.touched.bypassProxy && formik.errors.bypassProxy ? ( <div className="mb-3 flex items-center">
<div className="ml-3 text-red-500">{formik.errors.bypassProxy}</div> <label className="settings-label" htmlFor="auth.password">
) : null} Password
</div> </label>
<div className="textbox flex flex-row items-center w-[13.2rem] h-[2.25rem] relative">
<input
id="auth.password"
type={passwordVisible ? `text` : 'password'}
name="auth.password"
className="outline-none w-[10.5rem] bg-transparent"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={formik.values.auth.password}
onChange={formik.handleChange}
/>
<button
type="button"
className="btn btn-sm absolute right-0"
onClick={() => setPasswordVisible(!passwordVisible)}
>
{passwordVisible ? <IconEyeOff size={18} strokeWidth={2} /> : <IconEye size={18} strokeWidth={2} />}
</button>
</div>
{formik.touched.auth?.password && formik.errors.auth?.password ? (
<div className="ml-3 text-red-500">{formik.errors.auth.password}</div>
) : null}
</div>
</div>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="bypassProxy">
Proxy Bypass
</label>
<input
id="bypassProxy"
type="text"
name="bypassProxy"
className="block textbox"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={formik.handleChange}
value={formik.values.bypassProxy || ''}
/>
{formik.touched.bypassProxy && formik.errors.bypassProxy ? (
<div className="ml-3 text-red-500">{formik.errors.bypassProxy}</div>
) : null}
</div>
</>
) : null}
<div className="mt-6"> <div className="mt-6">
<button type="submit" className="submit btn btn-md btn-secondary"> <button type="submit" className="submit btn btn-md btn-secondary">
Save Save

View File

@ -1,39 +1,42 @@
import React from 'react'; import React from 'react';
import { IconSpeakerphone, IconBrandTwitter, IconBrandGithub, IconBrandDiscord, IconBook } from '@tabler/icons'; import { IconSpeakerphone, IconBrandTwitter, IconBrandGithub, IconBrandDiscord, IconBook } from '@tabler/icons';
import StyledWrapper from './StyledWrapper'; import StyledWrapper from './StyledWrapper';
import { useTranslation } from 'react-i18next';
const Support = () => { const Support = () => {
const { t } = useTranslation();
return ( return (
<StyledWrapper> <StyledWrapper>
<div className="rows"> <div className="rows">
<div className="mt-2"> <div className="mt-2">
<a href="https://docs.usebruno.com" target="_blank" className="flex items-end"> <a href="https://docs.usebruno.com" target="_blank" className="flex items-end">
<IconBook size={18} strokeWidth={2} /> <IconBook size={18} strokeWidth={2} />
<span className="label ml-2">Documentation</span> <span className="label ml-2">{t('COMMON.DOCUMENTATION')}</span>
</a> </a>
</div> </div>
<div className="mt-2"> <div className="mt-2">
<a href="https://github.com/usebruno/bruno/issues" target="_blank" className="flex items-end"> <a href="https://github.com/usebruno/bruno/issues" target="_blank" className="flex items-end">
<IconSpeakerphone size={18} strokeWidth={2} /> <IconSpeakerphone size={18} strokeWidth={2} />
<span className="label ml-2">Report Issues</span> <span className="label ml-2">{t('COMMON.REPORT_ISSUES')}</span>
</a> </a>
</div> </div>
<div className="mt-2"> <div className="mt-2">
<a href="https://discord.com/invite/KgcZUncpjq" target="_blank" className="flex items-end"> <a href="https://discord.com/invite/KgcZUncpjq" target="_blank" className="flex items-end">
<IconBrandDiscord size={18} strokeWidth={2} /> <IconBrandDiscord size={18} strokeWidth={2} />
<span className="label ml-2">Discord</span> <span className="label ml-2">{t('COMMON.DISCORD')}</span>
</a> </a>
</div> </div>
<div className="mt-2"> <div className="mt-2">
<a href="https://github.com/usebruno/bruno" target="_blank" className="flex items-end"> <a href="https://github.com/usebruno/bruno" target="_blank" className="flex items-end">
<IconBrandGithub size={18} strokeWidth={2} /> <IconBrandGithub size={18} strokeWidth={2} />
<span className="label ml-2">GitHub</span> <span className="label ml-2">{t('COMMON.GITHUB')}</span>
</a> </a>
</div> </div>
<div className="mt-2"> <div className="mt-2">
<a href="https://twitter.com/use_bruno" target="_blank" className="flex items-end"> <a href="https://twitter.com/use_bruno" target="_blank" className="flex items-end">
<IconBrandTwitter size={18} strokeWidth={2} /> <IconBrandTwitter size={18} strokeWidth={2} />
<span className="label ml-2">Twitter</span> <span className="label ml-2">{t('COMMON.TWITTER')}</span>
</a> </a>
</div> </div>
</div> </div>

View File

@ -0,0 +1,112 @@
import React, { useEffect, useRef, useState, useCallback } from 'react';
import { IconGripVertical, IconMinusVertical } from '@tabler/icons';
/**
* ReorderTable Component
*
* A table component that allows rows to be reordered via drag-and-drop.
*
* @param {Object} props - The component props
* @param {React.ReactNode[]} props.children - The table rows as children
* @param {function} props.updateReorderedItem - Callback function to handle reordered rows
*/
const ReorderTable = ({ children, updateReorderedItem }) => {
const tbodyRef = useRef();
const [rowsOrder, setRowsOrder] = useState(React.Children.toArray(children));
const [hoveredRow, setHoveredRow] = useState(null);
const [dragStart, setDragStart] = useState(null);
/**
* useEffect hook to update the rows order and handle row hover states
*/
useEffect(() => {
setRowsOrder(React.Children.toArray(children));
handleRowHover(null, false);
}, [children, dragStart]);
const handleRowHover = (index, hoverstatus = true) => {
setHoveredRow(hoverstatus ? index : null);
};
const handleDragStart = (e, index) => {
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', index);
setDragStart(index);
};
const handleDragOver = (e, index) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
handleRowHover(index);
};
const handleDrop = (e, toIndex) => {
e.preventDefault();
const fromIndex = parseInt(e.dataTransfer.getData('text/plain'), 10);
if (fromIndex !== toIndex) {
const updatedRowsOrder = [...rowsOrder];
const [movedRow] = updatedRowsOrder.splice(fromIndex, 1);
updatedRowsOrder.splice(toIndex, 0, movedRow);
setRowsOrder(updatedRowsOrder);
updateReorderedItem({
updateReorderedItem: updatedRowsOrder.map((row) => row.props['data-uid'])
});
setTimeout(() => {
handleRowHover(toIndex);
}, 0);
}
};
return (
<tbody ref={tbodyRef}>
{rowsOrder.map((row, index) => (
<tr
key={row.props['data-uid']}
data-uid={row.props['data-uid']}
draggable
onDragStart={(e) => handleDragStart(e, index)}
onDragOver={(e) => handleDragOver(e, index)}
onDrop={(e) => handleDrop(e, index)}
onMouseEnter={() => handleRowHover(index)}
onMouseLeave={() => handleRowHover(index, false)}
>
{React.Children.map(row.props.children, (child, childIndex) => {
if (childIndex === 0) {
return React.cloneElement(child, {
children: (
<>
<div
draggable
className="group drag-handle absolute z-10 left-[-17px] p-3.5 py-3.5 px-2.5 top-[3px] cursor-grab"
>
{hoveredRow === index && (
<>
<IconGripVertical
size={14}
className="z-10 icon-grip rounded-md absolute hidden group-hover:block"
/>
<IconMinusVertical
size={14}
className="z-10 icon-minus rounded-md absolute block group-hover:hidden"
/>
</>
)}
</div>
{child.props.children}
</>
)
});
} else {
return child;
}
})}
</tr>
))}
</tbody>
);
};
export default ReorderTable;

View File

@ -1,7 +1,4 @@
import React from 'react'; import React from 'react';
import { useTheme } from 'providers/Theme/index';
import darkTheme from 'themes/dark';
import lightTheme from 'themes/light';
/** /**
* Assertion operators * Assertion operators
@ -81,16 +78,10 @@ const AssertionOperator = ({ operator, onChange }) => {
} }
}; };
const { storedTheme } = useTheme();
return ( return (
<select value={operator} onChange={handleChange} className="mousetrap"> <select value={operator} onChange={handleChange} className="mousetrap">
{operators.map((operator) => ( {operators.map((operator) => (
<option <option key={operator} value={operator}>
style={{ backgroundColor: storedTheme === 'dark' ? darkTheme.bg : lightTheme.bg }}
key={operator}
value={operator}
>
{getLabel(operator)} {getLabel(operator)}
</option> </option>
))} ))}

View File

@ -191,6 +191,7 @@ const AssertionRow = ({
} }
onRun={handleRun} onRun={handleRun}
collection={collection} collection={collection}
item={item}
/> />
) : ( ) : (
<input type="text" className="cursor-default" disabled /> <input type="text" className="cursor-default" disabled />

View File

@ -55,6 +55,9 @@ const Wrapper = styled.div`
position: relative; position: relative;
top: 1px; top: 1px;
} }
option {
background-color: ${(props) => props.theme.bg};
}
`; `;
export default Wrapper; export default Wrapper;

View File

@ -136,6 +136,7 @@ const AwsV4Auth = ({ onTokenChange, item, collection }) => {
onChange={(val) => handleAccessKeyIdChange(val)} onChange={(val) => handleAccessKeyIdChange(val)}
onRun={handleRun} onRun={handleRun}
collection={collection} collection={collection}
item={item}
/> />
</div> </div>
@ -148,6 +149,8 @@ const AwsV4Auth = ({ onTokenChange, item, collection }) => {
onChange={(val) => handleSecretAccessKeyChange(val)} onChange={(val) => handleSecretAccessKeyChange(val)}
onRun={handleRun} onRun={handleRun}
collection={collection} collection={collection}
item={item}
isSecret={true}
/> />
</div> </div>
@ -160,6 +163,7 @@ const AwsV4Auth = ({ onTokenChange, item, collection }) => {
onChange={(val) => handleSessionTokenChange(val)} onChange={(val) => handleSessionTokenChange(val)}
onRun={handleRun} onRun={handleRun}
collection={collection} collection={collection}
item={item}
/> />
</div> </div>
@ -172,6 +176,7 @@ const AwsV4Auth = ({ onTokenChange, item, collection }) => {
onChange={(val) => handleServiceChange(val)} onChange={(val) => handleServiceChange(val)}
onRun={handleRun} onRun={handleRun}
collection={collection} collection={collection}
item={item}
/> />
</div> </div>
@ -184,6 +189,7 @@ const AwsV4Auth = ({ onTokenChange, item, collection }) => {
onChange={(val) => handleRegionChange(val)} onChange={(val) => handleRegionChange(val)}
onRun={handleRun} onRun={handleRun}
collection={collection} collection={collection}
item={item}
/> />
</div> </div>
@ -196,6 +202,7 @@ const AwsV4Auth = ({ onTokenChange, item, collection }) => {
onChange={(val) => handleProfileNameChange(val)} onChange={(val) => handleProfileNameChange(val)}
onRun={handleRun} onRun={handleRun}
collection={collection} collection={collection}
item={item}
/> />
</div> </div>
</StyledWrapper> </StyledWrapper>

View File

@ -55,6 +55,7 @@ const BasicAuth = ({ item, collection }) => {
onChange={(val) => handleUsernameChange(val)} onChange={(val) => handleUsernameChange(val)}
onRun={handleRun} onRun={handleRun}
collection={collection} collection={collection}
item={item}
/> />
</div> </div>
@ -67,6 +68,8 @@ const BasicAuth = ({ item, collection }) => {
onChange={(val) => handlePasswordChange(val)} onChange={(val) => handlePasswordChange(val)}
onRun={handleRun} onRun={handleRun}
collection={collection} collection={collection}
item={item}
isSecret={true}
/> />
</div> </div>
</StyledWrapper> </StyledWrapper>

View File

@ -42,6 +42,8 @@ const BearerAuth = ({ item, collection }) => {
onChange={(val) => handleTokenChange(val)} onChange={(val) => handleTokenChange(val)}
onRun={handleRun} onRun={handleRun}
collection={collection} collection={collection}
item={item}
isSecret={true}
/> />
</div> </div>
</StyledWrapper> </StyledWrapper>

View File

@ -55,6 +55,7 @@ const DigestAuth = ({ item, collection }) => {
onChange={(val) => handleUsernameChange(val)} onChange={(val) => handleUsernameChange(val)}
onRun={handleRun} onRun={handleRun}
collection={collection} collection={collection}
item={item}
/> />
</div> </div>
@ -67,6 +68,8 @@ const DigestAuth = ({ item, collection }) => {
onChange={(val) => handlePasswordChange(val)} onChange={(val) => handlePasswordChange(val)}
onRun={handleRun} onRun={handleRun}
collection={collection} collection={collection}
item={item}
isSecret={true}
/> />
</div> </div>
</StyledWrapper> </StyledWrapper>

View File

@ -80,7 +80,7 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
return ( return (
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col"> <StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
{inputsConfig.map((input) => { {inputsConfig.map((input) => {
const { key, label } = input; const { key, label, isSecret } = input;
return ( return (
<div className="flex flex-col w-full gap-1" key={`input-${key}`}> <div className="flex flex-col w-full gap-1" key={`input-${key}`}>
<label className="block font-medium">{label}</label> <label className="block font-medium">{label}</label>
@ -92,6 +92,8 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
onChange={(val) => handleChange(key, val)} onChange={(val) => handleChange(key, val)}
onRun={handleRun} onRun={handleRun}
collection={collection} collection={collection}
item={item}
isSecret={isSecret}
/> />
</div> </div>
</div> </div>

View File

@ -17,7 +17,8 @@ const inputsConfig = [
}, },
{ {
key: 'clientSecret', key: 'clientSecret',
label: 'Client Secret' label: 'Client Secret',
isSecret: true
}, },
{ {
key: 'scope', key: 'scope',

View File

@ -43,7 +43,7 @@ const OAuth2ClientCredentials = ({ item, collection }) => {
return ( return (
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col"> <StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
{inputsConfig.map((input) => { {inputsConfig.map((input) => {
const { key, label } = input; const { key, label, isSecret } = input;
return ( return (
<div className="flex flex-col w-full gap-1" key={`input-${key}`}> <div className="flex flex-col w-full gap-1" key={`input-${key}`}>
<label className="block font-medium">{label}</label> <label className="block font-medium">{label}</label>
@ -55,6 +55,8 @@ const OAuth2ClientCredentials = ({ item, collection }) => {
onChange={(val) => handleChange(key, val)} onChange={(val) => handleChange(key, val)}
onRun={handleRun} onRun={handleRun}
collection={collection} collection={collection}
item={item}
isSecret={isSecret}
/> />
</div> </div>
</div> </div>

View File

@ -9,7 +9,8 @@ const inputsConfig = [
}, },
{ {
key: 'clientSecret', key: 'clientSecret',
label: 'Client Secret' label: 'Client Secret',
isSecret: true
}, },
{ {
key: 'scope', key: 'scope',

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