Merge main into proxy branch (formatting and docs)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Gregory Schier
2026-03-13 12:09:59 -07:00
parent 3c4035097a
commit 7314aedc71
712 changed files with 13408 additions and 13322 deletions

View File

@@ -1,9 +1,11 @@
# Claude Context: Detaching Tauri from Yaak # Claude Context: Detaching Tauri from Yaak
## Goal ## Goal
Make Yaak runnable as a standalone CLI without Tauri as a dependency. The core Rust crates in `crates/` should be usable independently, while Tauri-specific code lives in `crates-tauri/`. Make Yaak runnable as a standalone CLI without Tauri as a dependency. The core Rust crates in `crates/` should be usable independently, while Tauri-specific code lives in `crates-tauri/`.
## Project Structure ## Project Structure
``` ```
crates/ # Core crates - should NOT depend on Tauri crates/ # Core crates - should NOT depend on Tauri
crates-tauri/ # Tauri-specific crates (yaak-app-client, yaak-tauri-utils, etc.) crates-tauri/ # Tauri-specific crates (yaak-app-client, yaak-tauri-utils, etc.)
@@ -13,11 +15,13 @@ crates-cli/ # CLI crate (yaak-cli)
## Completed Work ## Completed Work
### 1. Folder Restructure ### 1. Folder Restructure
- Moved Tauri-dependent app code to `crates-tauri/yaak-app-client/` - Moved Tauri-dependent app code to `crates-tauri/yaak-app-client/`
- Created `crates-tauri/yaak-tauri-utils/` for shared Tauri utilities (window traits, api_client, error handling) - Created `crates-tauri/yaak-tauri-utils/` for shared Tauri utilities (window traits, api_client, error handling)
- Created `crates-cli/yaak-cli/` for the standalone CLI - Created `crates-cli/yaak-cli/` for the standalone CLI
### 2. Decoupled Crates (no longer depend on Tauri) ### 2. Decoupled Crates (no longer depend on Tauri)
- **yaak-models**: Uses `init_standalone()` pattern for CLI database access - **yaak-models**: Uses `init_standalone()` pattern for CLI database access
- **yaak-http**: Removed Tauri plugin, HttpConnectionManager initialized in yaak-app setup - **yaak-http**: Removed Tauri plugin, HttpConnectionManager initialized in yaak-app setup
- **yaak-common**: Only contains Tauri-free utilities (serde, platform) - **yaak-common**: Only contains Tauri-free utilities (serde, platform)
@@ -25,6 +29,7 @@ crates-cli/ # CLI crate (yaak-cli)
- **yaak-grpc**: Replaced AppHandle with GrpcConfig struct, uses tokio::process::Command instead of Tauri sidecar - **yaak-grpc**: Replaced AppHandle with GrpcConfig struct, uses tokio::process::Command instead of Tauri sidecar
### 3. CLI Implementation ### 3. CLI Implementation
- Basic CLI at `crates-cli/yaak-cli/src/main.rs` - Basic CLI at `crates-cli/yaak-cli/src/main.rs`
- Commands: workspaces, requests, send (by ID), get (ad-hoc URL), create - Commands: workspaces, requests, send (by ID), get (ad-hoc URL), create
- Uses same database as Tauri app via `yaak_models::init_standalone()` - Uses same database as Tauri app via `yaak_models::init_standalone()`
@@ -32,12 +37,14 @@ crates-cli/ # CLI crate (yaak-cli)
## Remaining Work ## Remaining Work
### Crates Still Depending on Tauri (in `crates/`) ### Crates Still Depending on Tauri (in `crates/`)
1. **yaak-git** (3 files) - Moderate complexity 1. **yaak-git** (3 files) - Moderate complexity
2. **yaak-plugins** (13 files) - **Hardest** - deeply integrated with Tauri for plugin-to-window communication 2. **yaak-plugins** (13 files) - **Hardest** - deeply integrated with Tauri for plugin-to-window communication
3. **yaak-sync** (4 files) - Moderate complexity 3. **yaak-sync** (4 files) - Moderate complexity
4. **yaak-ws** (5 files) - Moderate complexity 4. **yaak-ws** (5 files) - Moderate complexity
### Pattern for Decoupling ### Pattern for Decoupling
1. Remove Tauri plugin `init()` function from the crate 1. Remove Tauri plugin `init()` function from the crate
2. Move commands to `yaak-app/src/commands.rs` or keep inline in `lib.rs` 2. Move commands to `yaak-app/src/commands.rs` or keep inline in `lib.rs`
3. Move extension traits (e.g., `SomethingManagerExt`) to yaak-app or yaak-tauri-utils 3. Move extension traits (e.g., `SomethingManagerExt`) to yaak-app or yaak-tauri-utils
@@ -47,6 +54,7 @@ crates-cli/ # CLI crate (yaak-cli)
7. Replace `tauri::async_runtime::block_on` with `tokio::runtime::Handle::current().block_on()` 7. Replace `tauri::async_runtime::block_on` with `tokio::runtime::Handle::current().block_on()`
## Key Files ## Key Files
- `crates-tauri/yaak-app-client/src/lib.rs` - Main Tauri app, setup block initializes managers - `crates-tauri/yaak-app-client/src/lib.rs` - Main Tauri app, setup block initializes managers
- `crates-tauri/yaak-app-client/src/commands.rs` - Migrated Tauri commands - `crates-tauri/yaak-app-client/src/commands.rs` - Migrated Tauri commands
- `crates-tauri/yaak-app-client/src/models_ext.rs` - Database plugin and extension traits - `crates-tauri/yaak-app-client/src/models_ext.rs` - Database plugin and extension traits
@@ -54,9 +62,11 @@ crates-cli/ # CLI crate (yaak-cli)
- `crates/yaak-models/src/lib.rs` - Contains `init_standalone()` for CLI usage - `crates/yaak-models/src/lib.rs` - Contains `init_standalone()` for CLI usage
## Git Branch ## Git Branch
Working on `detach-tauri` branch. Working on `detach-tauri` branch.
## Recent Commits ## Recent Commits
``` ```
c40cff40 Remove Tauri dependencies from yaak-crypto and yaak-grpc c40cff40 Remove Tauri dependencies from yaak-crypto and yaak-grpc
df495f1d Move Tauri utilities from yaak-common to yaak-tauri-utils df495f1d Move Tauri utilities from yaak-common to yaak-tauri-utils
@@ -67,6 +77,7 @@ e718a5f1 Refactor models_ext to use init_standalone from yaak-models
``` ```
## Testing ## Testing
- Run `cargo check -p <crate>` to verify a crate builds without Tauri - Run `cargo check -p <crate>` to verify a crate builds without Tauri
- Run `npm run client:dev` to test the Tauri app still works - Run `npm run client:dev` to test the Tauri app still works
- Run `cargo run -p yaak-cli -- --help` to test the CLI - Run `cargo run -p yaak-cli -- --help` to test the CLI

View File

@@ -8,7 +8,7 @@ Generate formatted release notes for Yaak releases by analyzing git history and
## What to do ## What to do
1. Identifies the version tag and previous version 1. Identifies the version tag and previous version
2. Retrieves all commits between versions 2. Retrieves all commits between versions
- If the version is a beta version, it retrieves commits between the beta version and previous beta version - If the version is a beta version, it retrieves commits between the beta version and previous beta version
- If the version is a stable version, it retrieves commits between the stable version and the previous stable version - If the version is a stable version, it retrieves commits between the stable version and the previous stable version
3. Fetches PR descriptions for linked issues to find: 3. Fetches PR descriptions for linked issues to find:

View File

@@ -1,10 +1,9 @@
--- ---
name: Bug report name: Bug report
about: Create a report to help us improve about: Create a report to help us improve
title: '' title: ""
labels: '' labels: ""
assignees: '' assignees: ""
--- ---
**Describe the bug** **Describe the bug**
@@ -12,6 +11,7 @@ A clear and concise description of what the bug is.
**To Reproduce** **To Reproduce**
Steps to reproduce the behavior: Steps to reproduce the behavior:
1. Go to '...' 1. Go to '...'
2. Click on '....' 2. Click on '....'
3. Scroll down to '....' 3. Scroll down to '....'
@@ -24,15 +24,17 @@ A clear and concise description of what you expected to happen.
If applicable, add screenshots to help explain your problem. If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):** **Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari] - OS: [e.g. iOS]
- Version [e.g. 22] - Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):** **Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1] - Device: [e.g. iPhone6]
- Browser [e.g. stock browser, safari] - OS: [e.g. iOS8.1]
- Version [e.g. 22] - Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context** **Additional context**
Add any other context about the problem here. Add any other context about the problem here.

View File

@@ -11,6 +11,7 @@
- [ ] I added or updated tests when reasonable. - [ ] I added or updated tests when reasonable.
Approved feedback item (required if not a bug fix or small-scope improvement): Approved feedback item (required if not a bug fix or small-scope improvement):
<!-- https://yaak.app/feedback/... --> <!-- https://yaak.app/feedback/... -->
## Related ## Related

View File

@@ -14,17 +14,20 @@ jobs:
runs-on: macos-latest runs-on: macos-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-node@v4 - uses: voidzero-dev/setup-vp@v1
with:
node-version: "24"
cache: true
- uses: dtolnay/rust-toolchain@stable - uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
with: with:
shared-key: ci shared-key: ci
cache-on-failure: true cache-on-failure: true
- run: npm ci - run: vp install
- run: npm run bootstrap - run: npm run bootstrap
- run: npm run lint - run: npm run lint
- name: Run JS Tests - name: Run JS Tests
run: npm test run: vp test
- name: Run Rust Tests - name: Run Rust Tests
run: cargo test --all run: cargo test --all

View File

@@ -47,4 +47,3 @@ jobs:
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
# or https://code.claude.com/docs/en/cli-reference for available options # or https://code.claude.com/docs/en/cli-reference for available options
# claude_args: '--allowed-tools Bash(gh pr:*)' # claude_args: '--allowed-tools Bash(gh pr:*)'

View File

@@ -50,8 +50,11 @@ jobs:
- name: Checkout yaakapp/app - name: Checkout yaakapp/app
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Setup Node - name: Setup Vite+
uses: actions/setup-node@v4 uses: voidzero-dev/setup-vp@v1
with:
node-version: "24"
cache: true
- name: install Rust stable - name: install Rust stable
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
@@ -87,13 +90,13 @@ jobs:
echo $dir >> $env:GITHUB_PATH echo $dir >> $env:GITHUB_PATH
& $exe --version & $exe --version
- run: npm ci - run: vp install
- run: npm run bootstrap - run: npm run bootstrap
env: env:
YAAK_TARGET_ARCH: ${{ matrix.yaak_arch }} YAAK_TARGET_ARCH: ${{ matrix.yaak_arch }}
- run: npm run lint - run: npm run lint
- name: Run JS Tests - name: Run JS Tests
run: npm test run: vp test
- name: Run Rust Tests - name: Run Rust Tests
run: cargo test --all --exclude yaak-cli run: cargo test --all --exclude yaak-cli

View File

@@ -16,23 +16,23 @@ jobs:
uses: JamesIves/github-sponsors-readme-action@v1 uses: JamesIves/github-sponsors-readme-action@v1
with: with:
token: ${{ secrets.SPONSORS_PAT }} token: ${{ secrets.SPONSORS_PAT }}
file: 'README.md' file: "README.md"
maximum: 1999 maximum: 1999
template: '<a href="https://github.com/{{{ login }}}"><img src="{{{ avatarUrl }}}" width="50px" alt="User avatar: {{{ login }}}" /></a>&nbsp;&nbsp;' template: '<a href="https://github.com/{{{ login }}}"><img src="{{{ avatarUrl }}}" width="50px" alt="User avatar: {{{ login }}}" /></a>&nbsp;&nbsp;'
active-only: false active-only: false
include-private: true include-private: true
marker: 'sponsors-base' marker: "sponsors-base"
- name: Generate Sponsors - name: Generate Sponsors
uses: JamesIves/github-sponsors-readme-action@v1 uses: JamesIves/github-sponsors-readme-action@v1
with: with:
token: ${{ secrets.SPONSORS_PAT }} token: ${{ secrets.SPONSORS_PAT }}
file: 'README.md' file: "README.md"
minimum: 2000 minimum: 2000
template: '<a href="https://github.com/{{{ login }}}"><img src="{{{ avatarUrl }}}" width="80px" alt="User avatar: {{{ login }}}" /></a>&nbsp;&nbsp;' template: '<a href="https://github.com/{{{ login }}}"><img src="{{{ avatarUrl }}}" width="80px" alt="User avatar: {{{ login }}}" /></a>&nbsp;&nbsp;'
active-only: false active-only: false
include-private: true include-private: true
marker: 'sponsors-premium' marker: "sponsors-premium"
# ⚠️ Note: You can use any deployment step here to automatically push the README # ⚠️ Note: You can use any deployment step here to automatically push the README
# changes back to your branch. # changes back to your branch.
@@ -41,4 +41,4 @@ jobs:
with: with:
branch: main branch: main
force: false force: false
folder: '.' folder: "."

1
.node-version Normal file
View File

@@ -0,0 +1 @@
24.14.0

2
.npmrc Normal file
View File

@@ -0,0 +1,2 @@
# vite-plugin-wasm has not yet declared Vite 8 in its peerDependencies
legacy-peer-deps=true

2
.oxfmtignore Normal file
View File

@@ -0,0 +1,2 @@
**/bindings/**
crates/yaak-templates/pkg/**

1
.vite-hooks/pre-commit Normal file
View File

@@ -0,0 +1 @@
vp lint

View File

@@ -1,3 +1,7 @@
{ {
"recommendations": ["biomejs.biome", "rust-lang.rust-analyzer", "bradlc.vscode-tailwindcss"] "recommendations": [
"rust-lang.rust-analyzer",
"bradlc.vscode-tailwindcss",
"VoidZero.vite-plus-extension-pack"
]
} }

View File

@@ -1,6 +1,8 @@
{ {
"editor.defaultFormatter": "biomejs.biome", "editor.defaultFormatter": "oxc.oxc-vscode",
"editor.formatOnSave": true, "editor.formatOnSave": true,
"biome.enabled": true, "editor.formatOnSaveMode": "file",
"biome.lint.format.enable": true "editor.codeActionsOnSave": {
"source.fixAll.oxc": "explicit"
}
} }

View File

@@ -1,38 +1,38 @@
[workspace] [workspace]
resolver = "2" resolver = "2"
members = [ members = [
"crates/yaak", "crates/yaak",
# Common/foundation crates # Common/foundation crates
"crates/common/yaak-database", "crates/common/yaak-database",
"crates/common/yaak-rpc", "crates/common/yaak-rpc",
# Shared crates (no Tauri dependency) # Shared crates (no Tauri dependency)
"crates/yaak-core", "crates/yaak-core",
"crates/yaak-common", "crates/yaak-common",
"crates/yaak-crypto", "crates/yaak-crypto",
"crates/yaak-git", "crates/yaak-git",
"crates/yaak-grpc", "crates/yaak-grpc",
"crates/yaak-http", "crates/yaak-http",
"crates/yaak-models", "crates/yaak-models",
"crates/yaak-plugins", "crates/yaak-plugins",
"crates/yaak-sse", "crates/yaak-sse",
"crates/yaak-sync", "crates/yaak-sync",
"crates/yaak-templates", "crates/yaak-templates",
"crates/yaak-tls", "crates/yaak-tls",
"crates/yaak-ws", "crates/yaak-ws",
"crates/yaak-api", "crates/yaak-api",
"crates/yaak-proxy", "crates/yaak-proxy",
# Proxy-specific crates # Proxy-specific crates
"crates-proxy/yaak-proxy-lib", "crates-proxy/yaak-proxy-lib",
# CLI crates # CLI crates
"crates-cli/yaak-cli", "crates-cli/yaak-cli",
# Tauri-specific crates # Tauri-specific crates
"crates-tauri/yaak-app-client", "crates-tauri/yaak-app-client",
"crates-tauri/yaak-app-proxy", "crates-tauri/yaak-app-proxy",
"crates-tauri/yaak-fonts", "crates-tauri/yaak-fonts",
"crates-tauri/yaak-license", "crates-tauri/yaak-license",
"crates-tauri/yaak-mac-window", "crates-tauri/yaak-mac-window",
"crates-tauri/yaak-tauri-utils", "crates-tauri/yaak-tauri-utils",
"crates-tauri/yaak-window", "crates-tauri/yaak-window",
] ]
[workspace.dependencies] [workspace.dependencies]

View File

@@ -1,24 +1,26 @@
# Developer Setup # Developer Setup
Yaak is a combined Node.js and Rust monorepo. It is a [Tauri](https://tauri.app) project, so Yaak is a combined Node.js and Rust monorepo. It is a [Tauri](https://tauri.app) project, so
uses Rust and HTML/CSS/JS for the main application but there is also a plugin system powered uses Rust and HTML/CSS/JS for the main application but there is also a plugin system powered
by a Node.js sidecar that communicates to the app over gRPC. by a Node.js sidecar that communicates to the app over gRPC.
Because of the moving parts, there are a few setup steps required before development can Because of the moving parts, there are a few setup steps required before development can
begin. begin.
## Prerequisites ## Prerequisites
Make sure you have the following tools installed: Make sure you have the following tools installed:
- [Node.js](https://nodejs.org/en/download/package-manager) - [Node.js](https://nodejs.org/en/download/package-manager) (v24+)
- [Rust](https://www.rust-lang.org/tools/install) - [Rust](https://www.rust-lang.org/tools/install)
- [Vite+](https://vite.dev/guide/vite-plus) (`vp` CLI)
Check the installations with the following commands: Check the installations with the following commands:
```shell ```shell
node -v node -v
npm -v npm -v
vp --version
rustc --version rustc --version
``` ```
@@ -45,12 +47,12 @@ npm start
## SQLite Migrations ## SQLite Migrations
New migrations can be created from the `src-tauri/` directory: New migrations can be created from the `src-tauri/` directory:
```shell ```shell
npm run migration npm run migration
``` ```
Rerun the app to apply the migrations. Rerun the app to apply the migrations.
_Note: For safety, development builds use a separate database location from production builds._ _Note: For safety, development builds use a separate database location from production builds._
@@ -61,9 +63,9 @@ _Note: For safety, development builds use a separate database location from prod
lezer-generator components/core/Editor/<LANG>/<LANG>.grammar > components/core/Editor/<LANG>/<LANG>.ts lezer-generator components/core/Editor/<LANG>/<LANG>.grammar > components/core/Editor/<LANG>/<LANG>.ts
``` ```
## Linting & Formatting ## Linting and Formatting
This repo uses Biome for linting and formatting (replacing ESLint + Prettier). This repo uses [Vite+](https://vite.dev/guide/vite-plus) for linting (oxlint) and formatting (oxfmt).
- Lint the entire repo: - Lint the entire repo:
@@ -71,12 +73,6 @@ This repo uses Biome for linting and formatting (replacing ESLint + Prettier).
npm run lint npm run lint
``` ```
- Auto-fix lint issues where possible:
```sh
npm run lint:fix
```
- Format code: - Format code:
```sh ```sh
@@ -84,5 +80,7 @@ npm run format
``` ```
Notes: Notes:
- Many workspace packages also expose the same scripts (`lint`, `lint:fix`, and `format`).
- TypeScript type-checking still runs separately via `tsc --noEmit` in relevant packages. - A pre-commit hook runs `vp lint` automatically on commit.
- Some workspace packages also run `tsc --noEmit` for type-checking.
- VS Code users should install the recommended extensions for format-on-save support.

View File

@@ -16,8 +16,6 @@
</p> </p>
<br> <br>
<p align="center"> <p align="center">
<!-- sponsors-premium --><a href="https://github.com/MVST-Solutions"><img src="https:&#x2F;&#x2F;github.com&#x2F;MVST-Solutions.png" width="80px" alt="User avatar: MVST-Solutions" /></a>&nbsp;&nbsp;<a href="https://github.com/dharsanb"><img src="https:&#x2F;&#x2F;github.com&#x2F;dharsanb.png" width="80px" alt="User avatar: dharsanb" /></a>&nbsp;&nbsp;<a href="https://github.com/railwayapp"><img src="https:&#x2F;&#x2F;github.com&#x2F;railwayapp.png" width="80px" alt="User avatar: railwayapp" /></a>&nbsp;&nbsp;<a href="https://github.com/caseyamcl"><img src="https:&#x2F;&#x2F;github.com&#x2F;caseyamcl.png" width="80px" alt="User avatar: caseyamcl" /></a>&nbsp;&nbsp;<a href="https://github.com/bytebase"><img src="https:&#x2F;&#x2F;github.com&#x2F;bytebase.png" width="80px" alt="User avatar: bytebase" /></a>&nbsp;&nbsp;<a href="https://github.com/"><img src="https:&#x2F;&#x2F;raw.githubusercontent.com&#x2F;JamesIves&#x2F;github-sponsors-readme-action&#x2F;dev&#x2F;.github&#x2F;assets&#x2F;placeholder.png" width="80px" alt="User avatar: " /></a>&nbsp;&nbsp;<!-- sponsors-premium --> <!-- sponsors-premium --><a href="https://github.com/MVST-Solutions"><img src="https:&#x2F;&#x2F;github.com&#x2F;MVST-Solutions.png" width="80px" alt="User avatar: MVST-Solutions" /></a>&nbsp;&nbsp;<a href="https://github.com/dharsanb"><img src="https:&#x2F;&#x2F;github.com&#x2F;dharsanb.png" width="80px" alt="User avatar: dharsanb" /></a>&nbsp;&nbsp;<a href="https://github.com/railwayapp"><img src="https:&#x2F;&#x2F;github.com&#x2F;railwayapp.png" width="80px" alt="User avatar: railwayapp" /></a>&nbsp;&nbsp;<a href="https://github.com/caseyamcl"><img src="https:&#x2F;&#x2F;github.com&#x2F;caseyamcl.png" width="80px" alt="User avatar: caseyamcl" /></a>&nbsp;&nbsp;<a href="https://github.com/bytebase"><img src="https:&#x2F;&#x2F;github.com&#x2F;bytebase.png" width="80px" alt="User avatar: bytebase" /></a>&nbsp;&nbsp;<a href="https://github.com/"><img src="https:&#x2F;&#x2F;raw.githubusercontent.com&#x2F;JamesIves&#x2F;github-sponsors-readme-action&#x2F;dev&#x2F;.github&#x2F;assets&#x2F;placeholder.png" width="80px" alt="User avatar: " /></a>&nbsp;&nbsp;<!-- sponsors-premium -->
</p> </p>
@@ -27,12 +25,10 @@
![Yaak API Client](https://yaak.app/static/screenshot.png) ![Yaak API Client](https://yaak.app/static/screenshot.png)
## Features ## Features
Yaak is an offline-first API client designed to stay out of your way while giving you everything you need when you need it. Yaak is an offline-first API client designed to stay out of your way while giving you everything you need when you need it.
Built with [Tauri](https://tauri.app), Rust, and React, its fast, lightweight, and private. No telemetry, no VC funding, and no cloud lock-in. Built with [Tauri](https://tauri.app), Rust, and React, its fast, lightweight, and private. No telemetry, no VC funding, and no cloud lock-in.
### 🌐 Work with any API ### 🌐 Work with any API
@@ -41,21 +37,23 @@ Built with [Tauri](https://tauri.app), Rust, and React, its fast, lightweight
- Filter and inspect responses with JSONPath or XPath. - Filter and inspect responses with JSONPath or XPath.
### 🔐 Stay secure ### 🔐 Stay secure
- Use OAuth 2.0, JWT, Basic Auth, or custom plugins for authentication. - Use OAuth 2.0, JWT, Basic Auth, or custom plugins for authentication.
- Secure sensitive values with encrypted secrets. - Secure sensitive values with encrypted secrets.
- Store secrets in your OS keychain. - Store secrets in your OS keychain.
### ☁️ Organize & collaborate ### ☁️ Organize & collaborate
- Group requests into workspaces and nested folders. - Group requests into workspaces and nested folders.
- Use environment variables to switch between dev, staging, and prod. - Use environment variables to switch between dev, staging, and prod.
- Mirror workspaces to your filesystem for versioning in Git or syncing with Dropbox. - Mirror workspaces to your filesystem for versioning in Git or syncing with Dropbox.
### 🧩 Extend & customize ### 🧩 Extend & customize
- Insert dynamic values like UUIDs or timestamps with template tags. - Insert dynamic values like UUIDs or timestamps with template tags.
- Pick from built-in themes or build your own. - Pick from built-in themes or build your own.
- Create plugins to extend authentication, template tags, or the UI. - Create plugins to extend authentication, template tags, or the UI.
## Contribution Policy ## Contribution Policy
> [!IMPORTANT] > [!IMPORTANT]

View File

@@ -1,6 +1,6 @@
import { createWorkspaceModel, type Folder, modelTypeLabel } from '@yaakapp-internal/models'; import { createWorkspaceModel, type Folder, modelTypeLabel } from "@yaakapp-internal/models";
import { applySync, calculateSync } from '@yaakapp-internal/sync'; import { applySync, calculateSync } from "@yaakapp-internal/sync";
import { Button } from '../components/core/Button'; import { Button } from "../components/core/Button";
import { import {
Banner, Banner,
InlineCode, InlineCode,
@@ -11,21 +11,21 @@ import {
TableHeaderCell, TableHeaderCell,
TableRow, TableRow,
TruncatedWideTableCell, TruncatedWideTableCell,
} from '@yaakapp-internal/ui'; } from "@yaakapp-internal/ui";
import { activeWorkspaceIdAtom } from '../hooks/useActiveWorkspace'; import { activeWorkspaceIdAtom } from "../hooks/useActiveWorkspace";
import { createFastMutation } from '../hooks/useFastMutation'; import { createFastMutation } from "../hooks/useFastMutation";
import { showDialog } from '../lib/dialog'; import { showDialog } from "../lib/dialog";
import { jotaiStore } from '../lib/jotai'; import { jotaiStore } from "../lib/jotai";
import { pluralizeCount } from '../lib/pluralize'; import { pluralizeCount } from "../lib/pluralize";
import { showPrompt } from '../lib/prompt'; import { showPrompt } from "../lib/prompt";
import { resolvedModelNameWithFolders } from '../lib/resolvedModelName'; import { resolvedModelNameWithFolders } from "../lib/resolvedModelName";
export const createFolder = createFastMutation< export const createFolder = createFastMutation<
string | null, string | null,
void, void,
Partial<Pick<Folder, 'name' | 'sortPriority' | 'folderId'>> Partial<Pick<Folder, "name" | "sortPriority" | "folderId">>
>({ >({
mutationKey: ['create_folder'], mutationKey: ["create_folder"],
mutationFn: async (patch) => { mutationFn: async (patch) => {
const workspaceId = jotaiStore.get(activeWorkspaceIdAtom); const workspaceId = jotaiStore.get(activeWorkspaceIdAtom);
if (workspaceId == null) { if (workspaceId == null) {
@@ -34,12 +34,12 @@ export const createFolder = createFastMutation<
if (!patch.name) { if (!patch.name) {
const name = await showPrompt({ const name = await showPrompt({
id: 'new-folder', id: "new-folder",
label: 'Name', label: "Name",
defaultValue: 'Folder', defaultValue: "Folder",
title: 'New Folder', title: "New Folder",
confirmText: 'Create', confirmText: "Create",
placeholder: 'Name', placeholder: "Name",
}); });
if (name == null) return null; if (name == null) return null;
@@ -47,7 +47,7 @@ export const createFolder = createFastMutation<
} }
patch.sortPriority = patch.sortPriority || -Date.now(); patch.sortPriority = patch.sortPriority || -Date.now();
const id = await createWorkspaceModel({ model: 'folder', workspaceId, ...patch }); const id = await createWorkspaceModel({ model: "folder", workspaceId, ...patch });
return id; return id;
}, },
}); });
@@ -61,12 +61,12 @@ export const syncWorkspace = createFastMutation<
mutationFn: async ({ workspaceId, syncDir, force }) => { mutationFn: async ({ workspaceId, syncDir, force }) => {
const ops = (await calculateSync(workspaceId, syncDir)) ?? []; const ops = (await calculateSync(workspaceId, syncDir)) ?? [];
if (ops.length === 0) { if (ops.length === 0) {
console.log('Nothing to sync', workspaceId, syncDir); console.log("Nothing to sync", workspaceId, syncDir);
return; return;
} }
console.log('Syncing workspace', workspaceId, syncDir, ops); console.log("Syncing workspace", workspaceId, syncDir, ops);
const dbOps = ops.filter((o) => o.type.startsWith('db')); const dbOps = ops.filter((o) => o.type.startsWith("db"));
if (dbOps.length === 0) { if (dbOps.length === 0) {
await applySync(workspaceId, syncDir, ops); await applySync(workspaceId, syncDir, ops);
@@ -74,10 +74,10 @@ export const syncWorkspace = createFastMutation<
} }
const isDeletingWorkspace = ops.some( const isDeletingWorkspace = ops.some(
(o) => o.type === 'dbDelete' && o.model.model === 'workspace', (o) => o.type === "dbDelete" && o.model.model === "workspace",
); );
console.log('Directory changes detected', { dbOps, ops }); console.log("Directory changes detected", { dbOps, ops });
if (force) { if (force) {
await applySync(workspaceId, syncDir, ops); await applySync(workspaceId, syncDir, ops);
@@ -85,9 +85,9 @@ export const syncWorkspace = createFastMutation<
} }
showDialog({ showDialog({
id: 'commit-sync', id: "commit-sync",
title: 'Changes Detected', title: "Changes Detected",
size: 'md', size: "md",
render: ({ hide }) => ( render: ({ hide }) => (
<form <form
className="h-full grid grid-rows-[auto_auto_minmax(0,1fr)_auto] gap-3" className="h-full grid grid-rows-[auto_auto_minmax(0,1fr)_auto] gap-3"
@@ -105,8 +105,8 @@ export const syncWorkspace = createFastMutation<
<span /> <span />
)} )}
<p> <p>
{pluralizeCount('file', dbOps.length)} in the directory{' '} {pluralizeCount("file", dbOps.length)} in the directory{" "}
{dbOps.length === 1 ? 'has' : 'have'} changed. Do you want to update your workspace? {dbOps.length === 1 ? "has" : "have"} changed. Do you want to update your workspace?
</p> </p>
<Table scrollable className="my-4"> <Table scrollable className="my-4">
<TableHead> <TableHead>
@@ -123,20 +123,20 @@ export const syncWorkspace = createFastMutation<
let color: string; let color: string;
let model: string; let model: string;
if (op.type === 'dbCreate') { if (op.type === "dbCreate") {
label = 'create'; label = "create";
name = resolvedModelNameWithFolders(op.fs.model); name = resolvedModelNameWithFolders(op.fs.model);
color = 'text-success'; color = "text-success";
model = modelTypeLabel(op.fs.model); model = modelTypeLabel(op.fs.model);
} else if (op.type === 'dbUpdate') { } else if (op.type === "dbUpdate") {
label = 'update'; label = "update";
name = resolvedModelNameWithFolders(op.fs.model); name = resolvedModelNameWithFolders(op.fs.model);
color = 'text-info'; color = "text-info";
model = modelTypeLabel(op.fs.model); model = modelTypeLabel(op.fs.model);
} else if (op.type === 'dbDelete') { } else if (op.type === "dbDelete") {
label = 'delete'; label = "delete";
name = resolvedModelNameWithFolders(op.model); name = resolvedModelNameWithFolders(op.model);
color = 'text-danger'; color = "text-danger";
model = modelTypeLabel(op.model); model = modelTypeLabel(op.model);
} else { } else {
return null; return null;

View File

@@ -1,33 +1,33 @@
import type { Environment } from '@yaakapp-internal/models'; import type { Environment } from "@yaakapp-internal/models";
import { CreateEnvironmentDialog } from '../components/CreateEnvironmentDialog'; import { CreateEnvironmentDialog } from "../components/CreateEnvironmentDialog";
import { activeWorkspaceIdAtom } from '../hooks/useActiveWorkspace'; import { activeWorkspaceIdAtom } from "../hooks/useActiveWorkspace";
import { createFastMutation } from '../hooks/useFastMutation'; import { createFastMutation } from "../hooks/useFastMutation";
import { showDialog } from '../lib/dialog'; import { showDialog } from "../lib/dialog";
import { jotaiStore } from '../lib/jotai'; import { jotaiStore } from "../lib/jotai";
import { setWorkspaceSearchParams } from '../lib/setWorkspaceSearchParams'; import { setWorkspaceSearchParams } from "../lib/setWorkspaceSearchParams";
export const createSubEnvironmentAndActivate = createFastMutation< export const createSubEnvironmentAndActivate = createFastMutation<
string | null, string | null,
unknown, unknown,
Environment | null Environment | null
>({ >({
mutationKey: ['create_environment'], mutationKey: ["create_environment"],
mutationFn: async (baseEnvironment) => { mutationFn: async (baseEnvironment) => {
if (baseEnvironment == null) { if (baseEnvironment == null) {
throw new Error('No base environment passed'); throw new Error("No base environment passed");
} }
const workspaceId = jotaiStore.get(activeWorkspaceIdAtom); const workspaceId = jotaiStore.get(activeWorkspaceIdAtom);
if (workspaceId == null) { if (workspaceId == null) {
throw new Error('Cannot create environment when no active workspace'); throw new Error("Cannot create environment when no active workspace");
} }
return new Promise<string | null>((resolve) => { return new Promise<string | null>((resolve) => {
showDialog({ showDialog({
id: 'new-environment', id: "new-environment",
title: 'New Environment', title: "New Environment",
description: 'Create multiple environments with different sets of variables', description: "Create multiple environments with different sets of variables",
size: 'sm', size: "sm",
onClose: () => resolve(null), onClose: () => resolve(null),
render: ({ hide }) => ( render: ({ hide }) => (
<CreateEnvironmentDialog <CreateEnvironmentDialog

View File

@@ -1,8 +1,8 @@
import type { WebsocketRequest } from '@yaakapp-internal/models'; import type { WebsocketRequest } from "@yaakapp-internal/models";
import { deleteWebsocketConnections as cmdDeleteWebsocketConnections } from '@yaakapp-internal/ws'; import { deleteWebsocketConnections as cmdDeleteWebsocketConnections } from "@yaakapp-internal/ws";
import { createFastMutation } from '../hooks/useFastMutation'; import { createFastMutation } from "../hooks/useFastMutation";
export const deleteWebsocketConnections = createFastMutation({ export const deleteWebsocketConnections = createFastMutation({
mutationKey: ['delete_websocket_connections'], mutationKey: ["delete_websocket_connections"],
mutationFn: async (request: WebsocketRequest) => cmdDeleteWebsocketConnections(request.id), mutationFn: async (request: WebsocketRequest) => cmdDeleteWebsocketConnections(request.id),
}); });

View File

@@ -1,28 +1,26 @@
import type { GrpcRequest, HttpRequest, WebsocketRequest } from '@yaakapp-internal/models'; import type { GrpcRequest, HttpRequest, WebsocketRequest } from "@yaakapp-internal/models";
import { MoveToWorkspaceDialog } from '../components/MoveToWorkspaceDialog'; import { MoveToWorkspaceDialog } from "../components/MoveToWorkspaceDialog";
import { activeWorkspaceIdAtom } from '../hooks/useActiveWorkspace'; import { activeWorkspaceIdAtom } from "../hooks/useActiveWorkspace";
import { createFastMutation } from '../hooks/useFastMutation'; import { createFastMutation } from "../hooks/useFastMutation";
import { pluralizeCount } from '../lib/pluralize'; import { pluralizeCount } from "../lib/pluralize";
import { showDialog } from '../lib/dialog'; import { showDialog } from "../lib/dialog";
import { jotaiStore } from '../lib/jotai'; import { jotaiStore } from "../lib/jotai";
export const moveToWorkspace = createFastMutation({ export const moveToWorkspace = createFastMutation({
mutationKey: ['move_workspace'], mutationKey: ["move_workspace"],
mutationFn: async (requests: (HttpRequest | GrpcRequest | WebsocketRequest)[]) => { mutationFn: async (requests: (HttpRequest | GrpcRequest | WebsocketRequest)[]) => {
const activeWorkspaceId = jotaiStore.get(activeWorkspaceIdAtom); const activeWorkspaceId = jotaiStore.get(activeWorkspaceIdAtom);
if (activeWorkspaceId == null) return; if (activeWorkspaceId == null) return;
if (requests.length === 0) return; if (requests.length === 0) return;
const title = const title =
requests.length === 1 requests.length === 1 ? "Move Request" : `Move ${pluralizeCount("Request", requests.length)}`;
? 'Move Request'
: `Move ${pluralizeCount('Request', requests.length)}`;
showDialog({ showDialog({
id: 'change-workspace', id: "change-workspace",
title, title,
size: 'sm', size: "sm",
render: ({ hide }) => ( render: ({ hide }) => (
<MoveToWorkspaceDialog <MoveToWorkspaceDialog
onDone={hide} onDone={hide}

View File

@@ -1,16 +1,16 @@
import { getModel } from '@yaakapp-internal/models'; import { getModel } from "@yaakapp-internal/models";
import type { FolderSettingsTab } from '../components/FolderSettingsDialog'; import type { FolderSettingsTab } from "../components/FolderSettingsDialog";
import { FolderSettingsDialog } from '../components/FolderSettingsDialog'; import { FolderSettingsDialog } from "../components/FolderSettingsDialog";
import { showDialog } from '../lib/dialog'; import { showDialog } from "../lib/dialog";
export function openFolderSettings(folderId: string, tab?: FolderSettingsTab) { export function openFolderSettings(folderId: string, tab?: FolderSettingsTab) {
const folder = getModel('folder', folderId); const folder = getModel("folder", folderId);
if (folder == null) return; if (folder == null) return;
showDialog({ showDialog({
id: 'folder-settings', id: "folder-settings",
title: null, title: null,
size: 'lg', size: "lg",
className: 'h-[50rem]', className: "h-[50rem]",
noPadding: true, noPadding: true,
render: () => <FolderSettingsDialog folderId={folderId} tab={tab} />, render: () => <FolderSettingsDialog folderId={folderId} tab={tab} />,
}); });

View File

@@ -1,29 +1,29 @@
import type { SettingsTab } from '../components/Settings/Settings'; import type { SettingsTab } from "../components/Settings/Settings";
import { activeWorkspaceIdAtom } from '../hooks/useActiveWorkspace'; import { activeWorkspaceIdAtom } from "../hooks/useActiveWorkspace";
import { createFastMutation } from '../hooks/useFastMutation'; import { createFastMutation } from "../hooks/useFastMutation";
import { jotaiStore } from '../lib/jotai'; import { jotaiStore } from "../lib/jotai";
import { router } from '../lib/router'; import { router } from "../lib/router";
import { invokeCmd } from '../lib/tauri'; import { invokeCmd } from "../lib/tauri";
// Allow tab with optional subtab (e.g., "plugins:installed") // Allow tab with optional subtab (e.g., "plugins:installed")
type SettingsTabWithSubtab = SettingsTab | `${SettingsTab}:${string}` | null; type SettingsTabWithSubtab = SettingsTab | `${SettingsTab}:${string}` | null;
export const openSettings = createFastMutation<void, string, SettingsTabWithSubtab>({ export const openSettings = createFastMutation<void, string, SettingsTabWithSubtab>({
mutationKey: ['open_settings'], mutationKey: ["open_settings"],
mutationFn: async (tab) => { mutationFn: async (tab) => {
const workspaceId = jotaiStore.get(activeWorkspaceIdAtom); const workspaceId = jotaiStore.get(activeWorkspaceIdAtom);
if (workspaceId == null) return; if (workspaceId == null) return;
const location = router.buildLocation({ const location = router.buildLocation({
to: '/workspaces/$workspaceId/settings', to: "/workspaces/$workspaceId/settings",
params: { workspaceId }, params: { workspaceId },
search: { tab: (tab ?? undefined) as SettingsTab | undefined }, search: { tab: (tab ?? undefined) as SettingsTab | undefined },
}); });
await invokeCmd('cmd_new_child_window', { await invokeCmd("cmd_new_child_window", {
url: location.href, url: location.href,
label: 'settings', label: "settings",
title: 'Yaak Settings', title: "Yaak Settings",
innerSize: [750, 600], innerSize: [750, 600],
}); });
}, },

View File

@@ -1,7 +1,7 @@
import { applySync, calculateSyncFsOnly } from '@yaakapp-internal/sync'; import { applySync, calculateSyncFsOnly } from "@yaakapp-internal/sync";
import { createFastMutation } from '../hooks/useFastMutation'; import { createFastMutation } from "../hooks/useFastMutation";
import { showSimpleAlert } from '../lib/alert'; import { showSimpleAlert } from "../lib/alert";
import { router } from '../lib/router'; import { router } from "../lib/router";
export const openWorkspaceFromSyncDir = createFastMutation<void, void, string>({ export const openWorkspaceFromSyncDir = createFastMutation<void, void, string>({
mutationKey: [], mutationKey: [],
@@ -9,18 +9,18 @@ export const openWorkspaceFromSyncDir = createFastMutation<void, void, string>({
const ops = await calculateSyncFsOnly(dir); const ops = await calculateSyncFsOnly(dir);
const workspace = ops const workspace = ops
.map((o) => (o.type === 'dbCreate' && o.fs.model.type === 'workspace' ? o.fs.model : null)) .map((o) => (o.type === "dbCreate" && o.fs.model.type === "workspace" ? o.fs.model : null))
.filter((m) => m)[0]; .filter((m) => m)[0];
if (workspace == null) { if (workspace == null) {
showSimpleAlert('Failed to Open', 'No workspace found in directory'); showSimpleAlert("Failed to Open", "No workspace found in directory");
return; return;
} }
await applySync(workspace.id, dir, ops); await applySync(workspace.id, dir, ops);
router.navigate({ await router.navigate({
to: '/workspaces/$workspaceId', to: "/workspaces/$workspaceId",
params: { workspaceId: workspace.id }, params: { workspaceId: workspace.id },
}); });
}, },

View File

@@ -1,16 +1,16 @@
import type { WorkspaceSettingsTab } from '../components/WorkspaceSettingsDialog'; import type { WorkspaceSettingsTab } from "../components/WorkspaceSettingsDialog";
import { WorkspaceSettingsDialog } from '../components/WorkspaceSettingsDialog'; import { WorkspaceSettingsDialog } from "../components/WorkspaceSettingsDialog";
import { activeWorkspaceIdAtom } from '../hooks/useActiveWorkspace'; import { activeWorkspaceIdAtom } from "../hooks/useActiveWorkspace";
import { showDialog } from '../lib/dialog'; import { showDialog } from "../lib/dialog";
import { jotaiStore } from '../lib/jotai'; import { jotaiStore } from "../lib/jotai";
export function openWorkspaceSettings(tab?: WorkspaceSettingsTab) { export function openWorkspaceSettings(tab?: WorkspaceSettingsTab) {
const workspaceId = jotaiStore.get(activeWorkspaceIdAtom); const workspaceId = jotaiStore.get(activeWorkspaceIdAtom);
if (workspaceId == null) return; if (workspaceId == null) return;
showDialog({ showDialog({
id: 'workspace-settings', id: "workspace-settings",
size: 'md', size: "md",
className: 'h-[calc(100vh-5rem)] !max-h-[40rem]', className: "h-[calc(100vh-5rem)] !max-h-[40rem]",
noPadding: true, noPadding: true,
render: ({ hide }) => ( render: ({ hide }) => (
<WorkspaceSettingsDialog workspaceId={workspaceId} hide={hide} tab={tab} /> <WorkspaceSettingsDialog workspaceId={workspaceId} hide={hide} tab={tab} />

View File

@@ -1,9 +1,9 @@
import { createFastMutation } from '../hooks/useFastMutation'; import { createFastMutation } from "../hooks/useFastMutation";
import { getRecentCookieJars } from '../hooks/useRecentCookieJars'; import { getRecentCookieJars } from "../hooks/useRecentCookieJars";
import { getRecentEnvironments } from '../hooks/useRecentEnvironments'; import { getRecentEnvironments } from "../hooks/useRecentEnvironments";
import { getRecentRequests } from '../hooks/useRecentRequests'; import { getRecentRequests } from "../hooks/useRecentRequests";
import { router } from '../lib/router'; import { router } from "../lib/router";
import { invokeCmd } from '../lib/tauri'; import { invokeCmd } from "../lib/tauri";
export const switchWorkspace = createFastMutation< export const switchWorkspace = createFastMutation<
void, void,
@@ -13,7 +13,7 @@ export const switchWorkspace = createFastMutation<
inNewWindow: boolean; inNewWindow: boolean;
} }
>({ >({
mutationKey: ['open_workspace'], mutationKey: ["open_workspace"],
mutationFn: async ({ workspaceId, inNewWindow }) => { mutationFn: async ({ workspaceId, inNewWindow }) => {
const environmentId = (await getRecentEnvironments(workspaceId))[0] ?? undefined; const environmentId = (await getRecentEnvironments(workspaceId))[0] ?? undefined;
const requestId = (await getRecentRequests(workspaceId))[0] ?? undefined; const requestId = (await getRecentRequests(workspaceId))[0] ?? undefined;
@@ -26,16 +26,16 @@ export const switchWorkspace = createFastMutation<
if (inNewWindow) { if (inNewWindow) {
const location = router.buildLocation({ const location = router.buildLocation({
to: '/workspaces/$workspaceId', to: "/workspaces/$workspaceId",
params: { workspaceId }, params: { workspaceId },
search, search,
}); });
await invokeCmd<void>('cmd_new_main_window', { url: location.href }); await invokeCmd<void>("cmd_new_main_window", { url: location.href });
return; return;
} }
await router.navigate({ await router.navigate({
to: '/workspaces/$workspaceId', to: "/workspaces/$workspaceId",
params: { workspaceId }, params: { workspaceId },
search, search,
}); });

View File

@@ -1,15 +1,15 @@
import type { HttpRequest } from '@yaakapp-internal/models'; import type { HttpRequest } from "@yaakapp-internal/models";
import { Banner, HStack, InlineCode, VStack } from '@yaakapp-internal/ui'; import { Banner, HStack, InlineCode, VStack } from "@yaakapp-internal/ui";
import mime from 'mime'; import mime from "mime";
import { useKeyValue } from '../hooks/useKeyValue'; import { useKeyValue } from "../hooks/useKeyValue";
import { Button } from './core/Button'; import { Button } from "./core/Button";
import { SelectFile } from './SelectFile'; import { SelectFile } from "./SelectFile";
type Props = { type Props = {
requestId: string; requestId: string;
contentType: string | null; contentType: string | null;
body: HttpRequest['body']; body: HttpRequest["body"];
onChange: (body: HttpRequest['body']) => void; onChange: (body: HttpRequest["body"]) => void;
onChangeContentType: (contentType: string | null) => void; onChangeContentType: (contentType: string | null) => void;
}; };
@@ -21,8 +21,8 @@ export function BinaryFileEditor({
requestId, requestId,
}: Props) { }: Props) {
const ignoreContentType = useKeyValue<boolean>({ const ignoreContentType = useKeyValue<boolean>({
namespace: 'global', namespace: "global",
key: ['ignore_content_type', requestId], key: ["ignore_content_type", requestId],
fallback: false, fallback: false,
}); });
@@ -31,8 +31,8 @@ export function BinaryFileEditor({
onChange({ filePath: filePath ?? undefined }); onChange({ filePath: filePath ?? undefined });
}; };
const filePath = typeof body.filePath === 'string' ? body.filePath : null; const filePath = typeof body.filePath === "string" ? body.filePath : null;
const mimeType = mime.getType(filePath ?? '') ?? 'application/octet-stream'; const mimeType = mime.getType(filePath ?? "") ?? "application/octet-stream";
return ( return (
<VStack space={2}> <VStack space={2}>

View File

@@ -1,12 +1,12 @@
import type { ReactNode } from 'react'; import type { ReactNode } from "react";
import { appInfo } from '../lib/appInfo'; import { appInfo } from "../lib/appInfo";
interface Props { interface Props {
children: ReactNode; children: ReactNode;
feature: 'updater' | 'license'; feature: "updater" | "license";
} }
const featureMap: Record<Props['feature'], boolean> = { const featureMap: Record<Props["feature"], boolean> = {
updater: appInfo.featureUpdater, updater: appInfo.featureUpdater,
license: appInfo.featureLicense, license: appInfo.featureLicense,
}; };

View File

@@ -1,15 +1,15 @@
import { open } from '@tauri-apps/plugin-dialog'; import { open } from "@tauri-apps/plugin-dialog";
import { gitClone } from '@yaakapp-internal/git'; import { gitClone } from "@yaakapp-internal/git";
import { Banner, VStack } from '@yaakapp-internal/ui'; import { Banner, VStack } from "@yaakapp-internal/ui";
import { useState } from 'react'; import { useState } from "react";
import { openWorkspaceFromSyncDir } from '../commands/openWorkspaceFromSyncDir'; import { openWorkspaceFromSyncDir } from "../commands/openWorkspaceFromSyncDir";
import { appInfo } from '../lib/appInfo'; import { appInfo } from "../lib/appInfo";
import { showErrorToast } from '../lib/toast'; import { showErrorToast } from "../lib/toast";
import { Button } from './core/Button'; import { Button } from "./core/Button";
import { Checkbox } from './core/Checkbox'; import { Checkbox } from "./core/Checkbox";
import { IconButton } from './core/IconButton'; import { IconButton } from "./core/IconButton";
import { PlainInput } from './core/PlainInput'; import { PlainInput } from "./core/PlainInput";
import { promptCredentials } from './git/credentials'; import { promptCredentials } from "./git/credentials";
interface Props { interface Props {
hide: () => void; hide: () => void;
@@ -17,15 +17,15 @@ interface Props {
// Detect path separator from an existing path (defaults to /) // Detect path separator from an existing path (defaults to /)
function getPathSeparator(path: string): string { function getPathSeparator(path: string): string {
return path.includes('\\') ? '\\' : '/'; return path.includes("\\") ? "\\" : "/";
} }
export function CloneGitRepositoryDialog({ hide }: Props) { export function CloneGitRepositoryDialog({ hide }: Props) {
const [url, setUrl] = useState<string>(''); const [url, setUrl] = useState<string>("");
const [baseDirectory, setBaseDirectory] = useState<string>(appInfo.defaultProjectDir); const [baseDirectory, setBaseDirectory] = useState<string>(appInfo.defaultProjectDir);
const [directoryOverride, setDirectoryOverride] = useState<string | null>(null); const [directoryOverride, setDirectoryOverride] = useState<string | null>(null);
const [hasSubdirectory, setHasSubdirectory] = useState(false); const [hasSubdirectory, setHasSubdirectory] = useState(false);
const [subdirectory, setSubdirectory] = useState<string>(''); const [subdirectory, setSubdirectory] = useState<string>("");
const [isCloning, setIsCloning] = useState(false); const [isCloning, setIsCloning] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@@ -38,7 +38,7 @@ export function CloneGitRepositoryDialog({ hide }: Props) {
const handleSelectDirectory = async () => { const handleSelectDirectory = async () => {
const dir = await open({ const dir = await open({
title: 'Select Directory', title: "Select Directory",
directory: true, directory: true,
multiple: false, multiple: false,
}); });
@@ -58,9 +58,9 @@ export function CloneGitRepositoryDialog({ hide }: Props) {
try { try {
const result = await gitClone(url, directory, promptCredentials); const result = await gitClone(url, directory, promptCredentials);
if (result.type === 'needs_credentials') { if (result.type === "needs_credentials") {
setError( setError(
result.error ?? 'Authentication failed. Please check your credentials and try again.', result.error ?? "Authentication failed. Please check your credentials and try again.",
); );
return; return;
} }
@@ -72,8 +72,8 @@ export function CloneGitRepositoryDialog({ hide }: Props) {
} catch (err) { } catch (err) {
setError(String(err)); setError(String(err));
showErrorToast({ showErrorToast({
id: 'git-clone-error', id: "git-clone-error",
title: 'Clone Failed', title: "Clone Failed",
message: String(err), message: String(err),
}); });
} finally { } finally {
@@ -136,7 +136,7 @@ export function CloneGitRepositoryDialog({ hide }: Props) {
disabled={!url || !directory || isCloning} disabled={!url || !directory || isCloning}
isLoading={isCloning} isLoading={isCloning}
> >
{isCloning ? 'Cloning...' : 'Clone Repository'} {isCloning ? "Cloning..." : "Clone Repository"}
</Button> </Button>
</VStack> </VStack>
); );
@@ -156,5 +156,5 @@ function extractRepoName(url: string): string {
if (sshMatch?.[1]) { if (sshMatch?.[1]) {
return sshMatch[1]; return sshMatch[1];
} }
return ''; return "";
} }

View File

@@ -1,5 +1,5 @@
import classNames from 'classnames'; import classNames from "classnames";
import type { CSSProperties } from 'react'; import type { CSSProperties } from "react";
interface Props { interface Props {
color: string | null; color: string | null;
@@ -11,7 +11,7 @@ export function ColorIndicator({ color, onClick, className }: Props) {
const style: CSSProperties = { backgroundColor: color ?? undefined }; const style: CSSProperties = { backgroundColor: color ?? undefined };
const finalClassName = classNames( const finalClassName = classNames(
className, className,
'inline-block w-[0.75em] h-[0.75em] rounded-full mr-1.5 border border-transparent flex-shrink-0', "inline-block w-[0.75em] h-[0.75em] rounded-full mr-1.5 border border-transparent flex-shrink-0",
); );
if (onClick) { if (onClick) {
@@ -20,7 +20,7 @@ export function ColorIndicator({ color, onClick, className }: Props) {
type="button" type="button"
onClick={onClick} onClick={onClick}
style={style} style={style}
className={classNames(finalClassName, 'hover:border-text')} className={classNames(finalClassName, "hover:border-text")}
/> />
); );
} }

View File

@@ -1,8 +1,8 @@
import { workspacesAtom } from '@yaakapp-internal/models'; import { workspacesAtom } from "@yaakapp-internal/models";
import { Heading, Icon, useDebouncedState } from '@yaakapp-internal/ui'; import { Heading, Icon, useDebouncedState } from "@yaakapp-internal/ui";
import classNames from 'classnames'; import classNames from "classnames";
import { fuzzyFilter } from 'fuzzbunny'; import { fuzzyFilter } from "fuzzbunny";
import { useAtomValue } from 'jotai'; import { useAtomValue } from "jotai";
import { import {
Fragment, Fragment,
type KeyboardEvent, type KeyboardEvent,
@@ -11,45 +11,45 @@ import {
useMemo, useMemo,
useRef, useRef,
useState, useState,
} from 'react'; } from "react";
import { createFolder } from '../commands/commands'; import { createFolder } from "../commands/commands";
import { createSubEnvironmentAndActivate } from '../commands/createEnvironment'; import { createSubEnvironmentAndActivate } from "../commands/createEnvironment";
import { openSettings } from '../commands/openSettings'; import { openSettings } from "../commands/openSettings";
import { switchWorkspace } from '../commands/switchWorkspace'; import { switchWorkspace } from "../commands/switchWorkspace";
import { useActiveCookieJar } from '../hooks/useActiveCookieJar'; import { useActiveCookieJar } from "../hooks/useActiveCookieJar";
import { useActiveEnvironment } from '../hooks/useActiveEnvironment'; import { useActiveEnvironment } from "../hooks/useActiveEnvironment";
import { useActiveRequest } from '../hooks/useActiveRequest'; import { useActiveRequest } from "../hooks/useActiveRequest";
import { activeWorkspaceIdAtom } from '../hooks/useActiveWorkspace'; import { activeWorkspaceIdAtom } from "../hooks/useActiveWorkspace";
import { useAllRequests } from '../hooks/useAllRequests'; import { useAllRequests } from "../hooks/useAllRequests";
import { useCreateWorkspace } from '../hooks/useCreateWorkspace'; import { useCreateWorkspace } from "../hooks/useCreateWorkspace";
import { useEnvironmentsBreakdown } from '../hooks/useEnvironmentsBreakdown'; import { useEnvironmentsBreakdown } from "../hooks/useEnvironmentsBreakdown";
import { useGrpcRequestActions } from '../hooks/useGrpcRequestActions'; import { useGrpcRequestActions } from "../hooks/useGrpcRequestActions";
import type { HotkeyAction } from '../hooks/useHotKey'; import type { HotkeyAction } from "../hooks/useHotKey";
import { useHttpRequestActions } from '../hooks/useHttpRequestActions'; import { useHttpRequestActions } from "../hooks/useHttpRequestActions";
import { useRecentEnvironments } from '../hooks/useRecentEnvironments'; import { useRecentEnvironments } from "../hooks/useRecentEnvironments";
import { useRecentRequests } from '../hooks/useRecentRequests'; import { useRecentRequests } from "../hooks/useRecentRequests";
import { useRecentWorkspaces } from '../hooks/useRecentWorkspaces'; import { useRecentWorkspaces } from "../hooks/useRecentWorkspaces";
import { useScrollIntoView } from '../hooks/useScrollIntoView'; import { useScrollIntoView } from "../hooks/useScrollIntoView";
import { useSendAnyHttpRequest } from '../hooks/useSendAnyHttpRequest'; import { useSendAnyHttpRequest } from "../hooks/useSendAnyHttpRequest";
import { useSidebarHidden } from '../hooks/useSidebarHidden'; import { useSidebarHidden } from "../hooks/useSidebarHidden";
import { appInfo } from '../lib/appInfo'; import { appInfo } from "../lib/appInfo";
import { copyToClipboard } from '../lib/copy'; import { copyToClipboard } from "../lib/copy";
import { createRequestAndNavigate } from '../lib/createRequestAndNavigate'; import { createRequestAndNavigate } from "../lib/createRequestAndNavigate";
import { deleteModelWithConfirm } from '../lib/deleteModelWithConfirm'; import { deleteModelWithConfirm } from "../lib/deleteModelWithConfirm";
import { showDialog } from '../lib/dialog'; import { showDialog } from "../lib/dialog";
import { editEnvironment } from '../lib/editEnvironment'; import { editEnvironment } from "../lib/editEnvironment";
import { renameModelWithPrompt } from '../lib/renameModelWithPrompt'; import { renameModelWithPrompt } from "../lib/renameModelWithPrompt";
import { import {
resolvedModelNameWithFolders, resolvedModelNameWithFolders,
resolvedModelNameWithFoldersArray, resolvedModelNameWithFoldersArray,
} from '../lib/resolvedModelName'; } from "../lib/resolvedModelName";
import { router } from '../lib/router'; import { router } from "../lib/router";
import { setWorkspaceSearchParams } from '../lib/setWorkspaceSearchParams'; import { setWorkspaceSearchParams } from "../lib/setWorkspaceSearchParams";
import { CookieDialog } from './CookieDialog'; import { CookieDialog } from "./CookieDialog";
import { Button } from './core/Button'; import { Button } from "./core/Button";
import { Hotkey } from './core/Hotkey'; import { Hotkey } from "./core/Hotkey";
import { HttpMethodTag } from './core/HttpMethodTag'; import { HttpMethodTag } from "./core/HttpMethodTag";
import { PlainInput } from './core/PlainInput'; import { PlainInput } from "./core/PlainInput";
interface CommandPaletteGroup { interface CommandPaletteGroup {
key: string; key: string;
@@ -66,7 +66,7 @@ type CommandPaletteItem = {
const MAX_PER_GROUP = 8; const MAX_PER_GROUP = 8;
export function CommandPaletteDialog({ onClose }: { onClose: () => void }) { export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
const [command, setCommand] = useDebouncedState<string>('', 150); const [command, setCommand] = useDebouncedState<string>("", 150);
const [selectedItemKey, setSelectedItemKey] = useState<string | null>(null); const [selectedItemKey, setSelectedItemKey] = useState<string | null>(null);
const activeEnvironment = useActiveEnvironment(); const activeEnvironment = useActiveEnvironment();
const httpRequestActions = useHttpRequestActions(); const httpRequestActions = useHttpRequestActions();
@@ -94,79 +94,79 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
const commands: CommandPaletteItem[] = [ const commands: CommandPaletteItem[] = [
{ {
key: 'settings.open', key: "settings.open",
label: 'Open Settings', label: "Open Settings",
action: 'settings.show', action: "settings.show",
onSelect: () => openSettings.mutate(null), onSelect: () => openSettings.mutate(null),
}, },
{ {
key: 'app.create', key: "app.create",
label: 'Create Workspace', label: "Create Workspace",
onSelect: createWorkspace, onSelect: createWorkspace,
}, },
{ {
key: 'model.create', key: "model.create",
label: 'Create HTTP Request', label: "Create HTTP Request",
onSelect: () => createRequestAndNavigate({ model: 'http_request', workspaceId }), onSelect: () => createRequestAndNavigate({ model: "http_request", workspaceId }),
}, },
{ {
key: 'grpc_request.create', key: "grpc_request.create",
label: 'Create GRPC Request', label: "Create GRPC Request",
onSelect: () => createRequestAndNavigate({ model: 'grpc_request', workspaceId }), onSelect: () => createRequestAndNavigate({ model: "grpc_request", workspaceId }),
}, },
{ {
key: 'websocket_request.create', key: "websocket_request.create",
label: 'Create Websocket Request', label: "Create Websocket Request",
onSelect: () => createRequestAndNavigate({ model: 'websocket_request', workspaceId }), onSelect: () => createRequestAndNavigate({ model: "websocket_request", workspaceId }),
}, },
{ {
key: 'folder.create', key: "folder.create",
label: 'Create Folder', label: "Create Folder",
onSelect: () => createFolder.mutate({}), onSelect: () => createFolder.mutate({}),
}, },
{ {
key: 'cookies.show', key: "cookies.show",
label: 'Show Cookies', label: "Show Cookies",
onSelect: async () => { onSelect: async () => {
showDialog({ showDialog({
id: 'cookies', id: "cookies",
title: 'Manage Cookies', title: "Manage Cookies",
size: 'full', size: "full",
render: () => <CookieDialog cookieJarId={activeCookieJar?.id ?? null} />, render: () => <CookieDialog cookieJarId={activeCookieJar?.id ?? null} />,
}); });
}, },
}, },
{ {
key: 'environment.edit', key: "environment.edit",
label: 'Edit Environment', label: "Edit Environment",
action: 'environment_editor.toggle', action: "environment_editor.toggle",
onSelect: () => editEnvironment(activeEnvironment), onSelect: () => editEnvironment(activeEnvironment),
}, },
{ {
key: 'environment.create', key: "environment.create",
label: 'Create Environment', label: "Create Environment",
onSelect: () => createSubEnvironmentAndActivate.mutate(baseEnvironment), onSelect: () => createSubEnvironmentAndActivate.mutate(baseEnvironment),
}, },
{ {
key: 'sidebar.toggle', key: "sidebar.toggle",
label: 'Toggle Sidebar', label: "Toggle Sidebar",
action: 'sidebar.focus', action: "sidebar.focus",
onSelect: () => setSidebarHidden((h) => !h), onSelect: () => setSidebarHidden((h) => !h),
}, },
]; ];
if (activeRequest?.model === 'http_request') { if (activeRequest?.model === "http_request") {
commands.push({ commands.push({
key: 'request.send', key: "request.send",
action: 'request.send', action: "request.send",
label: 'Send Request', label: "Send Request",
onSelect: () => sendRequest(activeRequest.id), onSelect: () => sendRequest(activeRequest.id),
}); });
if (appInfo.cliVersion != null) { if (appInfo.cliVersion != null) {
commands.push({ commands.push({
key: 'request.copy_cli_send', key: "request.copy_cli_send",
searchText: `copy cli send yaak request send ${activeRequest.id}`, searchText: `copy cli send yaak request send ${activeRequest.id}`,
label: 'Copy CLI Send Command', label: "Copy CLI Send Command",
onSelect: () => copyToClipboard(`yaak request send ${activeRequest.id}`), onSelect: () => copyToClipboard(`yaak request send ${activeRequest.id}`),
}); });
} }
@@ -179,7 +179,7 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
}); });
} }
if (activeRequest?.model === 'grpc_request') { if (activeRequest?.model === "grpc_request") {
grpcRequestActions.forEach((a, i) => { grpcRequestActions.forEach((a, i) => {
commands.push({ commands.push({
key: `grpc_request_action.${i}`, key: `grpc_request_action.${i}`,
@@ -191,21 +191,21 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
if (activeRequest != null) { if (activeRequest != null) {
commands.push({ commands.push({
key: 'http_request.rename', key: "http_request.rename",
label: 'Rename Request', label: "Rename Request",
onSelect: () => renameModelWithPrompt(activeRequest), onSelect: () => renameModelWithPrompt(activeRequest),
}); });
commands.push({ commands.push({
key: 'sidebar.selected.delete', key: "sidebar.selected.delete",
label: 'Delete Request', label: "Delete Request",
onSelect: () => deleteModelWithConfirm(activeRequest), onSelect: () => deleteModelWithConfirm(activeRequest),
}); });
} }
return commands.sort((a, b) => return commands.sort((a, b) =>
('searchText' in a ? a.searchText : a.label).localeCompare( ("searchText" in a ? a.searchText : a.label).localeCompare(
'searchText' in b ? b.searchText : b.label, "searchText" in b ? b.searchText : b.label,
), ),
); );
}, [ }, [
@@ -282,14 +282,14 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
const groups = useMemo<CommandPaletteGroup[]>(() => { const groups = useMemo<CommandPaletteGroup[]>(() => {
const actionsGroup: CommandPaletteGroup = { const actionsGroup: CommandPaletteGroup = {
key: 'actions', key: "actions",
label: 'Actions', label: "Actions",
items: workspaceCommands, items: workspaceCommands,
}; };
const requestGroup: CommandPaletteGroup = { const requestGroup: CommandPaletteGroup = {
key: 'requests', key: "requests",
label: 'Switch Request', label: "Switch Request",
items: [], items: [],
}; };
@@ -303,14 +303,14 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
{resolvedModelNameWithFoldersArray(r).map((name, i, all) => ( {resolvedModelNameWithFoldersArray(r).map((name, i, all) => (
<Fragment key={name}> <Fragment key={name}>
{i !== 0 && <Icon icon="chevron_right" className="opacity-80" />} {i !== 0 && <Icon icon="chevron_right" className="opacity-80" />}
<div className={classNames(i < all.length - 1 && 'truncate')}>{name}</div> <div className={classNames(i < all.length - 1 && "truncate")}>{name}</div>
</Fragment> </Fragment>
))} ))}
</div> </div>
), ),
onSelect: async () => { onSelect: async () => {
await router.navigate({ await router.navigate({
to: '/workspaces/$workspaceId', to: "/workspaces/$workspaceId",
params: { workspaceId: r.workspaceId }, params: { workspaceId: r.workspaceId },
search: (prev) => ({ ...prev, request_id: r.id }), search: (prev) => ({ ...prev, request_id: r.id }),
}); });
@@ -319,8 +319,8 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
} }
const environmentGroup: CommandPaletteGroup = { const environmentGroup: CommandPaletteGroup = {
key: 'environments', key: "environments",
label: 'Switch Environment', label: "Switch Environment",
items: [], items: [],
}; };
@@ -336,8 +336,8 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
} }
const workspaceGroup: CommandPaletteGroup = { const workspaceGroup: CommandPaletteGroup = {
key: 'workspaces', key: "workspaces",
label: 'Switch Workspace', label: "Switch Workspace",
items: [], items: [],
}; };
@@ -365,10 +365,10 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
? fuzzyFilter( ? fuzzyFilter(
allItems.map((i) => ({ allItems.map((i) => ({
...i, ...i,
filterBy: 'searchText' in i ? i.searchText : i.label, filterBy: "searchText" in i ? i.searchText : i.label,
})), })),
command, command,
{ fields: ['filterBy'] }, { fields: ["filterBy"] },
) )
.sort((a, b) => b.score - a.score) .sort((a, b) => b.score - a.score)
.map((v) => v.item) .map((v) => v.item)
@@ -406,13 +406,13 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
const handleKeyDown = useCallback( const handleKeyDown = useCallback(
(e: KeyboardEvent<HTMLInputElement>) => { (e: KeyboardEvent<HTMLInputElement>) => {
const index = filteredAllItems.findIndex((v) => v.key === selectedItem?.key); const index = filteredAllItems.findIndex((v) => v.key === selectedItem?.key);
if (e.key === 'ArrowDown' || (e.ctrlKey && e.key === 'n')) { if (e.key === "ArrowDown" || (e.ctrlKey && e.key === "n")) {
const next = filteredAllItems[index + 1] ?? filteredAllItems[0]; const next = filteredAllItems[index + 1] ?? filteredAllItems[0];
setSelectedItemKey(next?.key ?? null); setSelectedItemKey(next?.key ?? null);
} else if (e.key === 'ArrowUp' || (e.ctrlKey && e.key === 'k')) { } else if (e.key === "ArrowUp" || (e.ctrlKey && e.key === "k")) {
const prev = filteredAllItems[index - 1] ?? filteredAllItems[filteredAllItems.length - 1]; const prev = filteredAllItems[index - 1] ?? filteredAllItems[filteredAllItems.length - 1];
setSelectedItemKey(prev?.key ?? null); setSelectedItemKey(prev?.key ?? null);
} else if (e.key === 'Enter') { } else if (e.key === "Enter") {
const selected = filteredAllItems[index]; const selected = filteredAllItems[index];
setSelectedItemKey(selected?.key ?? null); setSelectedItemKey(selected?.key ?? null);
if (selected) { if (selected) {
@@ -489,10 +489,10 @@ function CommandPaletteItem({
color="custom" color="custom"
justify="start" justify="start"
className={classNames( className={classNames(
'w-full h-sm flex items-center rounded px-1.5', "w-full h-sm flex items-center rounded px-1.5",
'hover:text-text', "hover:text-text",
active && 'bg-surface-highlight', active && "bg-surface-highlight",
!active && 'text-text-subtle', !active && "text-text-subtle",
)} )}
> >
<span className="truncate">{children}</span> <span className="truncate">{children}</span>

View File

@@ -1,12 +1,12 @@
import type { HttpRequest } from '@yaakapp-internal/models'; import type { HttpRequest } from "@yaakapp-internal/models";
import { patchModel } from '@yaakapp-internal/models'; import { patchModel } from "@yaakapp-internal/models";
import { Banner, HStack, InlineCode } from '@yaakapp-internal/ui'; import { Banner, HStack, InlineCode } from "@yaakapp-internal/ui";
import type { ReactNode } from 'react'; import type { ReactNode } from "react";
import { useToggle } from '../hooks/useToggle'; import { useToggle } from "../hooks/useToggle";
import { showConfirm } from '../lib/confirm'; import { showConfirm } from "../lib/confirm";
import { Button } from './core/Button'; import { Button } from "./core/Button";
import { Link } from './core/Link'; import { Link } from "./core/Link";
import { SizeTag } from './core/SizeTag'; import { SizeTag } from "./core/SizeTag";
interface Props { interface Props {
children: ReactNode; children: ReactNode;
@@ -29,17 +29,17 @@ export function ConfirmLargeRequestBody({ children, request }: Props) {
return ( return (
<Banner color="primary" className="flex flex-col gap-3"> <Banner color="primary" className="flex flex-col gap-3">
<p> <p>
Rendering content over{' '} Rendering content over{" "}
<InlineCode> <InlineCode>
<SizeTag contentLength={tooLargeBytes} /> <SizeTag contentLength={tooLargeBytes} />
</InlineCode>{' '} </InlineCode>{" "}
may impact performance. may impact performance.
</p> </p>
<p> <p>
See{' '} See{" "}
<Link href="https://feedback.yaak.app/en/help/articles/1198684-working-with-large-values"> <Link href="https://feedback.yaak.app/en/help/articles/1198684-working-with-large-values">
Working With Large Values Working With Large Values
</Link>{' '} </Link>{" "}
for tips. for tips.
</p> </p>
<HStack wrap space={2}> <HStack wrap space={2}>
@@ -53,13 +53,13 @@ export function ConfirmLargeRequestBody({ children, request }: Props) {
onClick={async () => { onClick={async () => {
const confirm = await showConfirm({ const confirm = await showConfirm({
id: `delete-body-${request.id}`, id: `delete-body-${request.id}`,
confirmText: 'Delete Body', confirmText: "Delete Body",
title: 'Delete Body Text', title: "Delete Body Text",
description: 'Are you sure you want to delete the request body text?', description: "Are you sure you want to delete the request body text?",
color: 'danger', color: "danger",
}); });
if (confirm) { if (confirm) {
await patchModel(request, { body: { ...request.body, text: '' } }); await patchModel(request, { body: { ...request.body, text: "" } });
} }
}} }}
> >

View File

@@ -1,14 +1,14 @@
import type { HttpResponse } from '@yaakapp-internal/models'; import type { HttpResponse } from "@yaakapp-internal/models";
import { Banner, HStack, InlineCode } from '@yaakapp-internal/ui'; import { Banner, HStack, InlineCode } from "@yaakapp-internal/ui";
import { type ReactNode, useMemo } from 'react'; import { type ReactNode, useMemo } from "react";
import { useSaveResponse } from '../hooks/useSaveResponse'; import { useSaveResponse } from "../hooks/useSaveResponse";
import { useToggle } from '../hooks/useToggle'; import { useToggle } from "../hooks/useToggle";
import { isProbablyTextContentType } from '../lib/contentType'; import { isProbablyTextContentType } from "../lib/contentType";
import { getContentTypeFromHeaders } from '../lib/model_util'; import { getContentTypeFromHeaders } from "../lib/model_util";
import { getResponseBodyText } from '../lib/responseBody'; import { getResponseBodyText } from "../lib/responseBody";
import { CopyButton } from './CopyButton'; import { CopyButton } from "./CopyButton";
import { Button } from './core/Button'; import { Button } from "./core/Button";
import { SizeTag } from './core/SizeTag'; import { SizeTag } from "./core/SizeTag";
interface Props { interface Props {
children: ReactNode; children: ReactNode;
@@ -31,10 +31,10 @@ export function ConfirmLargeResponse({ children, response }: Props) {
return ( return (
<Banner color="primary" className="flex flex-col gap-3"> <Banner color="primary" className="flex flex-col gap-3">
<p> <p>
Showing responses over{' '} Showing responses over{" "}
<InlineCode> <InlineCode>
<SizeTag contentLength={LARGE_BYTES} /> <SizeTag contentLength={LARGE_BYTES} />
</InlineCode>{' '} </InlineCode>{" "}
may impact performance may impact performance
</p> </p>
<HStack wrap space={2}> <HStack wrap space={2}>

View File

@@ -1,13 +1,13 @@
import type { HttpResponse } from '@yaakapp-internal/models'; import type { HttpResponse } from "@yaakapp-internal/models";
import { Banner, HStack, InlineCode } from '@yaakapp-internal/ui'; import { Banner, HStack, InlineCode } from "@yaakapp-internal/ui";
import { type ReactNode, useMemo } from 'react'; import { type ReactNode, useMemo } from "react";
import { getRequestBodyText as getHttpResponseRequestBodyText } from '../hooks/useHttpRequestBody'; import { getRequestBodyText as getHttpResponseRequestBodyText } from "../hooks/useHttpRequestBody";
import { useToggle } from '../hooks/useToggle'; import { useToggle } from "../hooks/useToggle";
import { isProbablyTextContentType } from '../lib/contentType'; import { isProbablyTextContentType } from "../lib/contentType";
import { getContentTypeFromHeaders } from '../lib/model_util'; import { getContentTypeFromHeaders } from "../lib/model_util";
import { CopyButton } from './CopyButton'; import { CopyButton } from "./CopyButton";
import { Button } from './core/Button'; import { Button } from "./core/Button";
import { SizeTag } from './core/SizeTag'; import { SizeTag } from "./core/SizeTag";
interface Props { interface Props {
children: ReactNode; children: ReactNode;
@@ -29,10 +29,10 @@ export function ConfirmLargeResponseRequest({ children, response }: Props) {
return ( return (
<Banner color="primary" className="flex flex-col gap-3"> <Banner color="primary" className="flex flex-col gap-3">
<p> <p>
Showing content over{' '} Showing content over{" "}
<InlineCode> <InlineCode>
<SizeTag contentLength={LARGE_BYTES} /> <SizeTag contentLength={LARGE_BYTES} />
</InlineCode>{' '} </InlineCode>{" "}
may impact performance may impact performance
</p> </p>
<HStack wrap space={2}> <HStack wrap space={2}>
@@ -44,7 +44,7 @@ export function ConfirmLargeResponseRequest({ children, response }: Props) {
color="secondary" color="secondary"
variant="border" variant="border"
size="xs" size="xs"
text={() => getHttpResponseRequestBodyText(response).then((d) => d?.bodyText ?? '')} text={() => getHttpResponseRequestBodyText(response).then((d) => d?.bodyText ?? "")}
/> />
)} )}
</HStack> </HStack>

View File

@@ -1,9 +1,9 @@
import type { Cookie } from '@yaakapp-internal/models'; import type { Cookie } from "@yaakapp-internal/models";
import { cookieJarsAtom, patchModel } from '@yaakapp-internal/models'; import { cookieJarsAtom, patchModel } from "@yaakapp-internal/models";
import { useAtomValue } from 'jotai'; import { useAtomValue } from "jotai";
import { cookieDomain } from '../lib/model_util'; import { cookieDomain } from "../lib/model_util";
import { Banner, InlineCode } from '@yaakapp-internal/ui'; import { Banner, InlineCode } from "@yaakapp-internal/ui";
import { IconButton } from './core/IconButton'; import { IconButton } from "./core/IconButton";
interface Props { interface Props {
cookieJarId: string | null; cookieJarId: string | null;

View File

@@ -1,16 +1,16 @@
import { cookieJarsAtom, patchModel } from '@yaakapp-internal/models'; import { cookieJarsAtom, patchModel } from "@yaakapp-internal/models";
import { useAtomValue } from 'jotai'; import { useAtomValue } from "jotai";
import { memo, useMemo } from 'react'; import { memo, useMemo } from "react";
import { useActiveCookieJar } from '../hooks/useActiveCookieJar'; import { useActiveCookieJar } from "../hooks/useActiveCookieJar";
import { useCreateCookieJar } from '../hooks/useCreateCookieJar'; import { useCreateCookieJar } from "../hooks/useCreateCookieJar";
import { deleteModelWithConfirm } from '../lib/deleteModelWithConfirm'; import { deleteModelWithConfirm } from "../lib/deleteModelWithConfirm";
import { showDialog } from '../lib/dialog'; import { showDialog } from "../lib/dialog";
import { showPrompt } from '../lib/prompt'; import { showPrompt } from "../lib/prompt";
import { setWorkspaceSearchParams } from '../lib/setWorkspaceSearchParams'; import { setWorkspaceSearchParams } from "../lib/setWorkspaceSearchParams";
import { CookieDialog } from './CookieDialog'; import { CookieDialog } from "./CookieDialog";
import { Dropdown, type DropdownItem } from './core/Dropdown'; import { Dropdown, type DropdownItem } from "./core/Dropdown";
import { Icon, InlineCode } from '@yaakapp-internal/ui'; import { Icon, InlineCode } from "@yaakapp-internal/ui";
import { IconButton } from './core/IconButton'; import { IconButton } from "./core/IconButton";
export const CookieDropdown = memo(function CookieDropdown() { export const CookieDropdown = memo(function CookieDropdown() {
const activeCookieJar = useActiveCookieJar(); const activeCookieJar = useActiveCookieJar();
@@ -22,44 +22,44 @@ export const CookieDropdown = memo(function CookieDropdown() {
...(cookieJars ?? []).map((j) => ({ ...(cookieJars ?? []).map((j) => ({
key: j.id, key: j.id,
label: j.name, label: j.name,
leftSlot: <Icon icon={j.id === activeCookieJar?.id ? 'check' : 'empty'} />, leftSlot: <Icon icon={j.id === activeCookieJar?.id ? "check" : "empty"} />,
onSelect: () => { onSelect: () => {
setWorkspaceSearchParams({ cookie_jar_id: j.id }); setWorkspaceSearchParams({ cookie_jar_id: j.id });
}, },
})), })),
...(((cookieJars ?? []).length > 0 && activeCookieJar != null ...(((cookieJars ?? []).length > 0 && activeCookieJar != null
? [ ? [
{ type: 'separator', label: activeCookieJar.name }, { type: "separator", label: activeCookieJar.name },
{ {
key: 'manage', key: "manage",
label: 'Manage Cookies', label: "Manage Cookies",
leftSlot: <Icon icon="cookie" />, leftSlot: <Icon icon="cookie" />,
onSelect: () => { onSelect: () => {
if (activeCookieJar == null) return; if (activeCookieJar == null) return;
showDialog({ showDialog({
id: 'cookies', id: "cookies",
title: 'Manage Cookies', title: "Manage Cookies",
size: 'full', size: "full",
render: () => <CookieDialog cookieJarId={activeCookieJar.id} />, render: () => <CookieDialog cookieJarId={activeCookieJar.id} />,
}); });
}, },
}, },
{ {
key: 'rename', key: "rename",
label: 'Rename', label: "Rename",
leftSlot: <Icon icon="pencil" />, leftSlot: <Icon icon="pencil" />,
onSelect: async () => { onSelect: async () => {
const name = await showPrompt({ const name = await showPrompt({
id: 'rename-cookie-jar', id: "rename-cookie-jar",
title: 'Rename Cookie Jar', title: "Rename Cookie Jar",
description: ( description: (
<> <>
Enter a new name for <InlineCode>{activeCookieJar?.name}</InlineCode> Enter a new name for <InlineCode>{activeCookieJar?.name}</InlineCode>
</> </>
), ),
label: 'Name', label: "Name",
confirmText: 'Save', confirmText: "Save",
placeholder: 'New name', placeholder: "New name",
defaultValue: activeCookieJar?.name, defaultValue: activeCookieJar?.name,
}); });
if (name == null) return; if (name == null) return;
@@ -69,9 +69,9 @@ export const CookieDropdown = memo(function CookieDropdown() {
...(((cookieJars ?? []).length > 1 // Never delete the last one ...(((cookieJars ?? []).length > 1 // Never delete the last one
? [ ? [
{ {
label: 'Delete', label: "Delete",
leftSlot: <Icon icon="trash" />, leftSlot: <Icon icon="trash" />,
color: 'danger', color: "danger",
onSelect: async () => { onSelect: async () => {
await deleteModelWithConfirm(activeCookieJar); await deleteModelWithConfirm(activeCookieJar);
}, },
@@ -80,10 +80,10 @@ export const CookieDropdown = memo(function CookieDropdown() {
: []) as DropdownItem[]), : []) as DropdownItem[]),
] ]
: []) as DropdownItem[]), : []) as DropdownItem[]),
{ type: 'separator' }, { type: "separator" },
{ {
key: 'create-cookie-jar', key: "create-cookie-jar",
label: 'New Cookie Jar', label: "New Cookie Jar",
leftSlot: <Icon icon="plus" />, leftSlot: <Icon icon="plus" />,
onSelect: () => createCookieJar.mutate(), onSelect: () => createCookieJar.mutate(),
}, },

View File

@@ -1,10 +1,10 @@
import { useTimedBoolean } from '@yaakapp-internal/ui'; import { useTimedBoolean } from "@yaakapp-internal/ui";
import { copyToClipboard } from '../lib/copy'; import { copyToClipboard } from "../lib/copy";
import { showToast } from '../lib/toast'; import { showToast } from "../lib/toast";
import type { ButtonProps } from './core/Button'; import type { ButtonProps } from "./core/Button";
import { Button } from './core/Button'; import { Button } from "./core/Button";
interface Props extends Omit<ButtonProps, 'onClick'> { interface Props extends Omit<ButtonProps, "onClick"> {
text: string | (() => Promise<string | null>); text: string | (() => Promise<string | null>);
} }
@@ -14,12 +14,12 @@ export function CopyButton({ text, ...props }: Props) {
<Button <Button
{...props} {...props}
onClick={async () => { onClick={async () => {
const content = typeof text === 'function' ? await text() : text; const content = typeof text === "function" ? await text() : text;
if (content == null) { if (content == null) {
showToast({ showToast({
id: 'failed-to-copy', id: "failed-to-copy",
color: 'danger', color: "danger",
message: 'Failed to copy', message: "Failed to copy",
}); });
} else { } else {
copyToClipboard(content, { disableToast: true }); copyToClipboard(content, { disableToast: true });
@@ -27,7 +27,7 @@ export function CopyButton({ text, ...props }: Props) {
} }
}} }}
> >
{copied ? 'Copied' : 'Copy'} {copied ? "Copied" : "Copy"}
</Button> </Button>
); );
} }

View File

@@ -1,8 +1,8 @@
import { IconButton, type IconButtonProps, useTimedBoolean } from '@yaakapp-internal/ui'; import { IconButton, type IconButtonProps, useTimedBoolean } from "@yaakapp-internal/ui";
import { copyToClipboard } from '../lib/copy'; import { copyToClipboard } from "../lib/copy";
import { showToast } from '../lib/toast'; import { showToast } from "../lib/toast";
interface Props extends Omit<IconButtonProps, 'onClick' | 'icon'> { interface Props extends Omit<IconButtonProps, "onClick" | "icon"> {
text: string | (() => Promise<string | null>); text: string | (() => Promise<string | null>);
} }
@@ -11,15 +11,15 @@ export function CopyIconButton({ text, ...props }: Props) {
return ( return (
<IconButton <IconButton
{...props} {...props}
icon={copied ? 'check' : 'copy'} icon={copied ? "check" : "copy"}
showConfirm showConfirm
onClick={async () => { onClick={async () => {
const content = typeof text === 'function' ? await text() : text; const content = typeof text === "function" ? await text() : text;
if (content == null) { if (content == null) {
showToast({ showToast({
id: 'failed-to-copy', id: "failed-to-copy",
color: 'danger', color: "danger",
message: 'Failed to copy', message: "Failed to copy",
}); });
} else { } else {
copyToClipboard(content, { disableToast: true }); copyToClipboard(content, { disableToast: true });

View File

@@ -1,8 +1,8 @@
import { useCreateDropdownItems } from '../hooks/useCreateDropdownItems'; import { useCreateDropdownItems } from "../hooks/useCreateDropdownItems";
import type { DropdownProps } from './core/Dropdown'; import type { DropdownProps } from "./core/Dropdown";
import { Dropdown } from './core/Dropdown'; import { Dropdown } from "./core/Dropdown";
interface Props extends Omit<DropdownProps, 'items'> { interface Props extends Omit<DropdownProps, "items"> {
hideFolder?: boolean; hideFolder?: boolean;
} }
@@ -10,7 +10,7 @@ export function CreateDropdown({ hideFolder, children, ...props }: Props) {
const getItems = useCreateDropdownItems({ const getItems = useCreateDropdownItems({
hideFolder, hideFolder,
hideIcons: true, hideIcons: true,
folderId: 'active-folder', folderId: "active-folder",
}); });
return ( return (

View File

@@ -1,12 +1,12 @@
import { createWorkspaceModel } from '@yaakapp-internal/models'; import { createWorkspaceModel } from "@yaakapp-internal/models";
import { useState } from 'react'; import { useState } from "react";
import { useToggle } from '../hooks/useToggle'; import { useToggle } from "../hooks/useToggle";
import { ColorIndicator } from './ColorIndicator'; import { ColorIndicator } from "./ColorIndicator";
import { Button } from './core/Button'; import { Button } from "./core/Button";
import { Checkbox } from './core/Checkbox'; import { Checkbox } from "./core/Checkbox";
import { ColorPickerWithThemeColors } from './core/ColorPicker'; import { ColorPickerWithThemeColors } from "./core/ColorPicker";
import { Label } from './core/Label'; import { Label } from "./core/Label";
import { PlainInput } from './core/PlainInput'; import { PlainInput } from "./core/PlainInput";
interface Props { interface Props {
onCreate: (id: string) => void; onCreate: (id: string) => void;
@@ -15,7 +15,7 @@ interface Props {
} }
export function CreateEnvironmentDialog({ workspaceId, hide, onCreate }: Props) { export function CreateEnvironmentDialog({ workspaceId, hide, onCreate }: Props) {
const [name, setName] = useState<string>(''); const [name, setName] = useState<string>("");
const [color, setColor] = useState<string | null>(null); const [color, setColor] = useState<string | null>(null);
const [sharable, toggleSharable] = useToggle(false); const [sharable, toggleSharable] = useToggle(false);
return ( return (
@@ -24,13 +24,13 @@ export function CreateEnvironmentDialog({ workspaceId, hide, onCreate }: Props)
onSubmit={async (e) => { onSubmit={async (e) => {
e.preventDefault(); e.preventDefault();
const id = await createWorkspaceModel({ const id = await createWorkspaceModel({
model: 'environment', model: "environment",
name, name,
color, color,
variables: [], variables: [],
public: sharable, public: sharable,
workspaceId, workspaceId,
parentModel: 'environment', parentModel: "environment",
}); });
hide(); hide();
onCreate(id); onCreate(id);

View File

@@ -1,26 +1,26 @@
import { gitMutations } from '@yaakapp-internal/git'; import { gitMutations } from "@yaakapp-internal/git";
import type { WorkspaceMeta } from '@yaakapp-internal/models'; import type { WorkspaceMeta } from "@yaakapp-internal/models";
import { createGlobalModel, updateModel } from '@yaakapp-internal/models'; import { createGlobalModel, updateModel } from "@yaakapp-internal/models";
import { VStack } from '@yaakapp-internal/ui'; import { VStack } from "@yaakapp-internal/ui";
import { useState } from 'react'; import { useState } from "react";
import { router } from '../lib/router'; import { router } from "../lib/router";
import { setupOrConfigureEncryption } from '../lib/setupOrConfigureEncryption'; import { setupOrConfigureEncryption } from "../lib/setupOrConfigureEncryption";
import { invokeCmd } from '../lib/tauri'; import { invokeCmd } from "../lib/tauri";
import { showErrorToast } from '../lib/toast'; import { showErrorToast } from "../lib/toast";
import { Button } from './core/Button'; import { Button } from "./core/Button";
import { Checkbox } from './core/Checkbox'; import { Checkbox } from "./core/Checkbox";
import { Label } from './core/Label'; import { Label } from "./core/Label";
import { PlainInput } from './core/PlainInput'; import { PlainInput } from "./core/PlainInput";
import { EncryptionHelp } from './EncryptionHelp'; import { EncryptionHelp } from "./EncryptionHelp";
import { gitCallbacks } from './git/callbacks'; import { gitCallbacks } from "./git/callbacks";
import { SyncToFilesystemSetting } from './SyncToFilesystemSetting'; import { SyncToFilesystemSetting } from "./SyncToFilesystemSetting";
interface Props { interface Props {
hide: () => void; hide: () => void;
} }
export function CreateWorkspaceDialog({ hide }: Props) { export function CreateWorkspaceDialog({ hide }: Props) {
const [name, setName] = useState<string>(''); const [name, setName] = useState<string>("");
const [syncConfig, setSyncConfig] = useState<{ const [syncConfig, setSyncConfig] = useState<{
filePath: string | null; filePath: string | null;
initGit?: boolean; initGit?: boolean;
@@ -34,12 +34,12 @@ export function CreateWorkspaceDialog({ hide }: Props) {
className="pb-3" className="pb-3"
onSubmit={async (e) => { onSubmit={async (e) => {
e.preventDefault(); e.preventDefault();
const workspaceId = await createGlobalModel({ model: 'workspace', name }); const workspaceId = await createGlobalModel({ model: "workspace", name });
if (workspaceId == null) return; if (workspaceId == null) return;
// Do getWorkspaceMeta instead of naively creating one because it might have // Do getWorkspaceMeta instead of naively creating one because it might have
// been created already when the store refreshes the workspace meta after // been created already when the store refreshes the workspace meta after
const workspaceMeta = await invokeCmd<WorkspaceMeta>('cmd_get_workspace_meta', { const workspaceMeta = await invokeCmd<WorkspaceMeta>("cmd_get_workspace_meta", {
workspaceId, workspaceId,
}); });
await updateModel({ await updateModel({
@@ -52,8 +52,8 @@ export function CreateWorkspaceDialog({ hide }: Props) {
.init.mutateAsync() .init.mutateAsync()
.catch((err) => { .catch((err) => {
showErrorToast({ showErrorToast({
id: 'git-init-error', id: "git-init-error",
title: 'Error initializing Git', title: "Error initializing Git",
message: String(err), message: String(err),
}); });
}); });
@@ -61,7 +61,7 @@ export function CreateWorkspaceDialog({ hide }: Props) {
// Navigate to workspace // Navigate to workspace
await router.navigate({ await router.navigate({
to: '/workspaces/$workspaceId', to: "/workspaces/$workspaceId",
params: { workspaceId }, params: { workspaceId },
}); });

View File

@@ -1,14 +1,14 @@
import { useAtomValue } from 'jotai'; import { useAtomValue } from "jotai";
import type { ComponentType } from 'react'; import type { ComponentType } from "react";
import { useCallback } from 'react'; import { useCallback } from "react";
import { dialogsAtom, hideDialog } from '../lib/dialog'; import { dialogsAtom, hideDialog } from "../lib/dialog";
import { Dialog, type DialogProps } from './core/Dialog'; import { Dialog, type DialogProps } from "./core/Dialog";
import { ErrorBoundary } from './ErrorBoundary'; import { ErrorBoundary } from "./ErrorBoundary";
export type DialogInstance = { export type DialogInstance = {
id: string; id: string;
render: ComponentType<{ hide: () => void }>; render: ComponentType<{ hide: () => void }>;
} & Omit<DialogProps, 'open' | 'children'>; } & Omit<DialogProps, "open" | "children">;
export function Dialogs() { export function Dialogs() {
const dialogs = useAtomValue(dialogsAtom); const dialogs = useAtomValue(dialogsAtom);

View File

@@ -1,5 +1,5 @@
import type { DnsOverride, Workspace } from '@yaakapp-internal/models'; import type { DnsOverride, Workspace } from "@yaakapp-internal/models";
import { patchModel } from '@yaakapp-internal/models'; import { patchModel } from "@yaakapp-internal/models";
import { import {
HStack, HStack,
Table, Table,
@@ -9,12 +9,12 @@ import {
TableHeaderCell, TableHeaderCell,
TableRow, TableRow,
VStack, VStack,
} from '@yaakapp-internal/ui'; } from "@yaakapp-internal/ui";
import { useCallback, useId, useMemo } from 'react'; import { useCallback, useId, useMemo } from "react";
import { Button } from './core/Button'; import { Button } from "./core/Button";
import { Checkbox } from './core/Checkbox'; import { Checkbox } from "./core/Checkbox";
import { IconButton } from './core/IconButton'; import { IconButton } from "./core/IconButton";
import { PlainInput } from './core/PlainInput'; import { PlainInput } from "./core/PlainInput";
interface Props { interface Props {
workspace: Workspace; workspace: Workspace;
@@ -44,8 +44,8 @@ export function DnsOverridesEditor({ workspace }: Props) {
const handleAdd = useCallback(() => { const handleAdd = useCallback(() => {
const newOverride: DnsOverride = { const newOverride: DnsOverride = {
hostname: '', hostname: "",
ipv4: [''], ipv4: [""],
ipv6: [], ipv6: [],
enabled: true, enabled: true,
}; };
@@ -73,7 +73,7 @@ export function DnsOverridesEditor({ workspace }: Props) {
return ( return (
<VStack space={3} className="pb-3"> <VStack space={3} className="pb-3">
<div className="text-text-subtle text-sm"> <div className="text-text-subtle text-sm">
Override DNS resolution for specific hostnames. This works like{' '} Override DNS resolution for specific hostnames. This works like{" "}
<code className="text-text-subtlest bg-surface-highlight px-1 rounded">/etc/hosts</code> but <code className="text-text-subtlest bg-surface-highlight px-1 rounded">/etc/hosts</code> but
only for requests made from this workspace. only for requests made from this workspace.
</div> </div>
@@ -118,15 +118,15 @@ interface DnsOverrideRowProps {
} }
function DnsOverrideRow({ override, onUpdate, onDelete }: DnsOverrideRowProps) { function DnsOverrideRow({ override, onUpdate, onDelete }: DnsOverrideRowProps) {
const ipv4Value = override.ipv4.join(', '); const ipv4Value = override.ipv4.join(", ");
const ipv6Value = override.ipv6.join(', '); const ipv6Value = override.ipv6.join(", ");
return ( return (
<TableRow> <TableRow>
<TableCell> <TableCell>
<Checkbox <Checkbox
hideLabel hideLabel
title={override.enabled ? 'Disable override' : 'Enable override'} title={override.enabled ? "Disable override" : "Enable override"}
checked={override.enabled ?? true} checked={override.enabled ?? true}
onChange={(enabled) => onUpdate({ enabled })} onChange={(enabled) => onUpdate({ enabled })}
/> />
@@ -151,7 +151,7 @@ function DnsOverrideRow({ override, onUpdate, onDelete }: DnsOverrideRowProps) {
onChange={(value) => onChange={(value) =>
onUpdate({ onUpdate({
ipv4: value ipv4: value
.split(',') .split(",")
.map((s) => s.trim()) .map((s) => s.trim())
.filter(Boolean), .filter(Boolean),
}) })
@@ -168,7 +168,7 @@ function DnsOverrideRow({ override, onUpdate, onDelete }: DnsOverrideRowProps) {
onChange={(value) => onChange={(value) =>
onUpdate({ onUpdate({
ipv6: value ipv6: value
.split(',') .split(",")
.map((s) => s.trim()) .map((s) => s.trim())
.filter(Boolean), .filter(Boolean),
}) })

View File

@@ -1,30 +1,30 @@
import classNames from 'classnames'; import classNames from "classnames";
import type { CSSProperties } from 'react'; import type { CSSProperties } from "react";
import { memo } from 'react'; import { memo } from "react";
interface Props { interface Props {
className?: string; className?: string;
style?: CSSProperties; style?: CSSProperties;
orientation?: 'horizontal' | 'vertical'; orientation?: "horizontal" | "vertical";
} }
export const DropMarker = memo( export const DropMarker = memo(
function DropMarker({ className, style, orientation = 'horizontal' }: Props) { function DropMarker({ className, style, orientation = "horizontal" }: Props) {
return ( return (
<div <div
style={style} style={style}
className={classNames( className={classNames(
className, className,
'absolute pointer-events-none z-50', "absolute pointer-events-none z-50",
orientation === 'horizontal' && 'w-full', orientation === "horizontal" && "w-full",
orientation === 'vertical' && 'w-0 top-0 bottom-0', orientation === "vertical" && "w-0 top-0 bottom-0",
)} )}
> >
<div <div
className={classNames( className={classNames(
'absolute bg-primary rounded-full', "absolute bg-primary rounded-full",
orientation === 'horizontal' && 'left-2 right-2 -bottom-[0.1rem] h-[0.2rem]', orientation === "horizontal" && "left-2 right-2 -bottom-[0.1rem] h-[0.2rem]",
orientation === 'vertical' && '-left-[0.1rem] top-0 bottom-0 w-[0.2rem]', orientation === "vertical" && "-left-[0.1rem] top-0 bottom-0 w-[0.2rem]",
)} )}
/> />
</div> </div>

View File

@@ -1,5 +1,5 @@
import type { Folder, HttpRequest } from '@yaakapp-internal/models'; import type { Folder, HttpRequest } from "@yaakapp-internal/models";
import { foldersAtom, httpRequestsAtom } from '@yaakapp-internal/models'; import { foldersAtom, httpRequestsAtom } from "@yaakapp-internal/models";
import type { import type {
FormInput, FormInput,
FormInputCheckbox, FormInputCheckbox,
@@ -10,32 +10,32 @@ import type {
FormInputSelect, FormInputSelect,
FormInputText, FormInputText,
JsonPrimitive, JsonPrimitive,
} from '@yaakapp-internal/plugins'; } from "@yaakapp-internal/plugins";
import { Banner, VStack } from '@yaakapp-internal/ui'; import { Banner, VStack } from "@yaakapp-internal/ui";
import classNames from 'classnames'; import classNames from "classnames";
import { useAtomValue } from 'jotai'; import { useAtomValue } from "jotai";
import { useCallback, useEffect, useMemo } from 'react'; import { useCallback, useEffect, useMemo } from "react";
import { useActiveRequest } from '../hooks/useActiveRequest'; import { useActiveRequest } from "../hooks/useActiveRequest";
import { useRandomKey } from '../hooks/useRandomKey'; import { useRandomKey } from "../hooks/useRandomKey";
import { capitalize } from '../lib/capitalize'; import { capitalize } from "../lib/capitalize";
import { showDialog } from '../lib/dialog'; import { showDialog } from "../lib/dialog";
import { resolvedModelName } from '../lib/resolvedModelName'; import { resolvedModelName } from "../lib/resolvedModelName";
import { Checkbox } from './core/Checkbox'; import { Checkbox } from "./core/Checkbox";
import { DetailsBanner } from './core/DetailsBanner'; import { DetailsBanner } from "./core/DetailsBanner";
import { Editor } from './core/Editor/LazyEditor'; import { Editor } from "./core/Editor/LazyEditor";
import { IconButton } from './core/IconButton'; import { IconButton } from "./core/IconButton";
import type { InputProps } from './core/Input'; import type { InputProps } from "./core/Input";
import { Input } from './core/Input'; import { Input } from "./core/Input";
import { Label } from './core/Label'; import { Label } from "./core/Label";
import type { Pair } from './core/PairEditor'; import type { Pair } from "./core/PairEditor";
import { PairEditor } from './core/PairEditor'; import { PairEditor } from "./core/PairEditor";
import { PlainInput } from './core/PlainInput'; import { PlainInput } from "./core/PlainInput";
import { Select } from './core/Select'; import { Select } from "./core/Select";
import { Markdown } from './Markdown'; import { Markdown } from "./Markdown";
import { SelectFile } from './SelectFile'; import { SelectFile } from "./SelectFile";
export const DYNAMIC_FORM_NULL_ARG = '__NULL__'; export const DYNAMIC_FORM_NULL_ARG = "__NULL__";
const INPUT_SIZE = 'sm'; const INPUT_SIZE = "sm";
interface Props<T> { interface Props<T> {
inputs: FormInput[] | undefined | null; inputs: FormInput[] | undefined | null;
@@ -74,7 +74,7 @@ export function DynamicForm<T extends Record<string, JsonPrimitive>>({
autocompleteFunctions={autocompleteFunctions} autocompleteFunctions={autocompleteFunctions}
autocompleteVariables={autocompleteVariables} autocompleteVariables={autocompleteVariables}
data={data} data={data}
className={classNames(className, 'pb-4')} // Pad the bottom to look nice className={classNames(className, "pb-4")} // Pad the bottom to look nice
/> />
); );
} }
@@ -88,8 +88,8 @@ function FormInputsStack<T extends Record<string, JsonPrimitive>>({
space={3} space={3}
className={classNames( className={classNames(
className, className,
'h-full overflow-auto', "h-full overflow-auto",
'pr-1', // A bit of space between inputs and scrollbar "pr-1", // A bit of space between inputs and scrollbar
)} )}
> >
<FormInputs {...props} /> <FormInputs {...props} />
@@ -99,7 +99,7 @@ function FormInputsStack<T extends Record<string, JsonPrimitive>>({
type FormInputsProps<T> = Pick< type FormInputsProps<T> = Pick<
Props<T>, Props<T>,
'inputs' | 'autocompleteFunctions' | 'autocompleteVariables' | 'stateKey' | 'data' "inputs" | "autocompleteFunctions" | "autocompleteVariables" | "stateKey" | "data"
> & { > & {
setDataAttr: (name: string, value: JsonPrimitive) => void; setDataAttr: (name: string, value: JsonPrimitive) => void;
disabled?: boolean; disabled?: boolean;
@@ -117,16 +117,16 @@ function FormInputs<T extends Record<string, JsonPrimitive>>({
return ( return (
<> <>
{inputs?.map((input, i) => { {inputs?.map((input, i) => {
if ('hidden' in input && input.hidden) { if ("hidden" in input && input.hidden) {
return null; return null;
} }
if ('disabled' in input && disabled != null) { if ("disabled" in input && disabled != null) {
input.disabled = disabled; input.disabled = disabled;
} }
switch (input.type) { switch (input.type) {
case 'select': case "select":
return ( return (
<SelectArg <SelectArg
key={i + stateKey} key={i + stateKey}
@@ -139,7 +139,7 @@ function FormInputs<T extends Record<string, JsonPrimitive>>({
} }
/> />
); );
case 'text': case "text":
return ( return (
<TextArg <TextArg
key={i + stateKey} key={i + stateKey}
@@ -149,11 +149,11 @@ function FormInputs<T extends Record<string, JsonPrimitive>>({
autocompleteVariables={autocompleteVariables || false} autocompleteVariables={autocompleteVariables || false}
onChange={(v) => setDataAttr(input.name, v)} onChange={(v) => setDataAttr(input.name, v)}
value={ value={
data[input.name] != null ? String(data[input.name]) : (input.defaultValue ?? '') data[input.name] != null ? String(data[input.name]) : (input.defaultValue ?? "")
} }
/> />
); );
case 'editor': case "editor":
return ( return (
<EditorArg <EditorArg
key={i + stateKey} key={i + stateKey}
@@ -163,11 +163,11 @@ function FormInputs<T extends Record<string, JsonPrimitive>>({
autocompleteVariables={autocompleteVariables || false} autocompleteVariables={autocompleteVariables || false}
onChange={(v) => setDataAttr(input.name, v)} onChange={(v) => setDataAttr(input.name, v)}
value={ value={
data[input.name] != null ? String(data[input.name]) : (input.defaultValue ?? '') data[input.name] != null ? String(data[input.name]) : (input.defaultValue ?? "")
} }
/> />
); );
case 'checkbox': case "checkbox":
return ( return (
<CheckboxArg <CheckboxArg
key={i + stateKey} key={i + stateKey}
@@ -176,7 +176,7 @@ function FormInputs<T extends Record<string, JsonPrimitive>>({
value={data[input.name] != null ? data[input.name] === true : false} value={data[input.name] != null ? data[input.name] === true : false}
/> />
); );
case 'http_request': case "http_request":
return ( return (
<HttpRequestArg <HttpRequestArg
key={i + stateKey} key={i + stateKey}
@@ -185,7 +185,7 @@ function FormInputs<T extends Record<string, JsonPrimitive>>({
value={data[input.name] != null ? String(data[input.name]) : DYNAMIC_FORM_NULL_ARG} value={data[input.name] != null ? String(data[input.name]) : DYNAMIC_FORM_NULL_ARG}
/> />
); );
case 'file': case "file":
return ( return (
<FileArg <FileArg
key={i + stateKey} key={i + stateKey}
@@ -196,7 +196,7 @@ function FormInputs<T extends Record<string, JsonPrimitive>>({
} }
/> />
); );
case 'accordion': case "accordion":
if (!hasVisibleInputs(input.inputs)) { if (!hasVisibleInputs(input.inputs)) {
return null; return null;
} }
@@ -204,7 +204,7 @@ function FormInputs<T extends Record<string, JsonPrimitive>>({
<div key={i + stateKey}> <div key={i + stateKey}>
<DetailsBanner <DetailsBanner
summary={input.label} summary={input.label}
className={classNames('!mb-auto', disabled && 'opacity-disabled')} className={classNames("!mb-auto", disabled && "opacity-disabled")}
> >
<div className="mt-3"> <div className="mt-3">
<FormInputsStack <FormInputsStack
@@ -220,7 +220,7 @@ function FormInputs<T extends Record<string, JsonPrimitive>>({
</DetailsBanner> </DetailsBanner>
</div> </div>
); );
case 'h_stack': case "h_stack":
if (!hasVisibleInputs(input.inputs)) { if (!hasVisibleInputs(input.inputs)) {
return null; return null;
} }
@@ -237,7 +237,7 @@ function FormInputs<T extends Record<string, JsonPrimitive>>({
/> />
</div> </div>
); );
case 'banner': case "banner":
if (!hasVisibleInputs(input.inputs)) { if (!hasVisibleInputs(input.inputs)) {
return null; return null;
} }
@@ -245,7 +245,7 @@ function FormInputs<T extends Record<string, JsonPrimitive>>({
<Banner <Banner
key={i + stateKey} key={i + stateKey}
color={input.color} color={input.color}
className={classNames(disabled && 'opacity-disabled')} className={classNames(disabled && "opacity-disabled")}
> >
<FormInputsStack <FormInputsStack
data={data} data={data}
@@ -258,9 +258,9 @@ function FormInputs<T extends Record<string, JsonPrimitive>>({
/> />
</Banner> </Banner>
); );
case 'markdown': case "markdown":
return <Markdown key={i + stateKey}>{input.content}</Markdown>; return <Markdown key={i + stateKey}>{input.content}</Markdown>;
case 'key_value': case "key_value":
return ( return (
<KeyValueArg <KeyValueArg
key={i + stateKey} key={i + stateKey}
@@ -268,7 +268,7 @@ function FormInputs<T extends Record<string, JsonPrimitive>>({
stateKey={stateKey} stateKey={stateKey}
onChange={(v) => setDataAttr(input.name, v)} onChange={(v) => setDataAttr(input.name, v)}
value={ value={
data[input.name] != null ? String(data[input.name]) : (input.defaultValue ?? '[]') data[input.name] != null ? String(data[input.name]) : (input.defaultValue ?? "[]")
} }
/> />
); );
@@ -300,12 +300,12 @@ function TextArg({
onChange, onChange,
name: arg.name, name: arg.name,
multiLine: arg.multiLine, multiLine: arg.multiLine,
className: arg.multiLine ? 'min-h-[4rem]' : undefined, className: arg.multiLine ? "min-h-[4rem]" : undefined,
defaultValue: value === DYNAMIC_FORM_NULL_ARG ? arg.defaultValue : value, defaultValue: value === DYNAMIC_FORM_NULL_ARG ? arg.defaultValue : value,
required: !arg.optional, required: !arg.optional,
disabled: arg.disabled, disabled: arg.disabled,
help: arg.description, help: arg.description,
type: arg.password ? 'password' : 'text', type: arg.password ? "password" : "text",
label: arg.label ?? arg.name, label: arg.label ?? arg.name,
size: INPUT_SIZE, size: INPUT_SIZE,
hideLabel: arg.hideLabel ?? arg.label == null, hideLabel: arg.hideLabel ?? arg.label == null,
@@ -357,9 +357,9 @@ function EditorArg({
</Label> </Label>
<div <div
className={classNames( className={classNames(
'border border-border rounded-md overflow-hidden px-2 py-1', "border border-border rounded-md overflow-hidden px-2 py-1",
'focus-within:border-border-focus', "focus-within:border-border-focus",
!arg.rows && 'max-h-[10rem]', // So it doesn't take up too much space !arg.rows && "max-h-[10rem]", // So it doesn't take up too much space
)} )}
style={arg.rows ? { height: `${arg.rows * 1.4 + 0.75}rem` } : undefined} style={arg.rows ? { height: `${arg.rows * 1.4 + 0.75}rem` } : undefined}
> >
@@ -389,10 +389,10 @@ function EditorArg({
title="Pop out to large editor" title="Pop out to large editor"
onClick={() => { onClick={() => {
showDialog({ showDialog({
id: 'id', id: "id",
size: 'full', size: "full",
title: arg.readOnly ? 'View Value' : 'Edit Value', title: arg.readOnly ? "View Value" : "Edit Value",
className: '!max-w-[50rem] !max-h-[60rem]', className: "!max-w-[50rem] !max-h-[60rem]",
description: arg.label && ( description: arg.label && (
<Label <Label
htmlFor={id} htmlFor={id}
@@ -495,7 +495,7 @@ function HttpRequestArg({
}) { }) {
const folders = useAtomValue(foldersAtom); const folders = useAtomValue(foldersAtom);
const httpRequests = useAtomValue(httpRequestsAtom); const httpRequests = useAtomValue(httpRequestsAtom);
const activeHttpRequest = useActiveRequest('http_request'); const activeHttpRequest = useActiveRequest("http_request");
useEffect(() => { useEffect(() => {
if (value === DYNAMIC_FORM_NULL_ARG && activeHttpRequest) { if (value === DYNAMIC_FORM_NULL_ARG && activeHttpRequest) {
@@ -515,8 +515,8 @@ function HttpRequestArg({
...httpRequests.map((r) => { ...httpRequests.map((r) => {
return { return {
label: label:
buildRequestBreadcrumbs(r, folders).join(' / ') + buildRequestBreadcrumbs(r, folders).join(" / ") +
(r.id === activeHttpRequest?.id ? ' (current)' : ''), (r.id === activeHttpRequest?.id ? " (current)" : ""),
value: r.id, value: r.id,
}; };
}), }),
@@ -540,7 +540,7 @@ function buildRequestBreadcrumbs(request: HttpRequest, folders: Folder[]): strin
}; };
next(); next();
return ancestors.map((a) => (a.model === 'folder' ? a.name : resolvedModelName(a))); return ancestors.map((a) => (a.model === "folder" ? a.name : resolvedModelName(a)));
} }
function CheckboxArg({ function CheckboxArg({
@@ -617,7 +617,7 @@ function hasVisibleInputs(inputs: FormInput[] | undefined): boolean {
if (!inputs) return false; if (!inputs) return false;
for (const input of inputs) { for (const input of inputs) {
if ('inputs' in input && !hasVisibleInputs(input.inputs)) { if ("inputs" in input && !hasVisibleInputs(input.inputs)) {
// Has children, but none are visible // Has children, but none are visible
return false; return false;
} }

View File

@@ -1,5 +1,5 @@
import classNames from 'classnames'; import classNames from "classnames";
import type { ReactNode } from 'react'; import type { ReactNode } from "react";
interface Props { interface Props {
children: ReactNode; children: ReactNode;
@@ -12,8 +12,8 @@ export function EmptyStateText({ children, className }: Props) {
<div <div
className={classNames( className={classNames(
className, className,
'rounded-lg border border-dashed border-border-subtle', "rounded-lg border border-dashed border-border-subtle",
'h-full py-2 text-text-subtlest flex items-center justify-center italic', "h-full py-2 text-text-subtlest flex items-center justify-center italic",
)} )}
> >
{children} {children}

View File

@@ -1,4 +1,4 @@
import { VStack } from '@yaakapp-internal/ui'; import { VStack } from "@yaakapp-internal/ui";
export function EncryptionHelp() { export function EncryptionHelp() {
return ( return (

View File

@@ -1,19 +1,19 @@
import classNames from 'classnames'; import classNames from "classnames";
import { memo, useMemo } from 'react'; import { memo, useMemo } from "react";
import { useActiveEnvironment } from '../hooks/useActiveEnvironment'; import { useActiveEnvironment } from "../hooks/useActiveEnvironment";
import { useEnvironmentsBreakdown } from '../hooks/useEnvironmentsBreakdown'; import { useEnvironmentsBreakdown } from "../hooks/useEnvironmentsBreakdown";
import { editEnvironment } from '../lib/editEnvironment'; import { editEnvironment } from "../lib/editEnvironment";
import { setWorkspaceSearchParams } from '../lib/setWorkspaceSearchParams'; import { setWorkspaceSearchParams } from "../lib/setWorkspaceSearchParams";
import type { ButtonProps } from './core/Button'; import type { ButtonProps } from "./core/Button";
import { Button } from './core/Button'; import { Button } from "./core/Button";
import type { DropdownItem } from './core/Dropdown'; import type { DropdownItem } from "./core/Dropdown";
import { Dropdown } from './core/Dropdown'; import { Dropdown } from "./core/Dropdown";
import { Icon } from '@yaakapp-internal/ui'; import { Icon } from "@yaakapp-internal/ui";
import { EnvironmentColorIndicator } from './EnvironmentColorIndicator'; import { EnvironmentColorIndicator } from "./EnvironmentColorIndicator";
type Props = { type Props = {
className?: string; className?: string;
} & Pick<ButtonProps, 'forDropdown' | 'leftSlot'>; } & Pick<ButtonProps, "forDropdown" | "leftSlot">;
export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdown({ export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdown({
className, className,
@@ -41,11 +41,11 @@ export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdo
[activeEnvironment?.id], [activeEnvironment?.id],
), ),
...((subEnvironments.length > 0 ...((subEnvironments.length > 0
? [{ type: 'separator', label: 'Environments' }] ? [{ type: "separator", label: "Environments" }]
: []) as DropdownItem[]), : []) as DropdownItem[]),
{ {
label: 'Manage Environments', label: "Manage Environments",
hotKeyAction: 'environment_editor.toggle', hotKeyAction: "environment_editor.toggle",
leftSlot: <Icon icon="box" />, leftSlot: <Icon icon="box" />,
onSelect: () => editEnvironment(activeEnvironment), onSelect: () => editEnvironment(activeEnvironment),
}, },
@@ -62,8 +62,8 @@ export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdo
size="sm" size="sm"
className={classNames( className={classNames(
className, className,
'text !px-2 truncate', "text !px-2 truncate",
!activeEnvironment && !hasBaseVars && 'text-text-subtlest italic', !activeEnvironment && !hasBaseVars && "text-text-subtlest italic",
)} )}
// If no environments, the button simply opens the dialog. // If no environments, the button simply opens the dialog.
// NOTE: We don't create a new button because we want to reuse the hotkey from the menu items // NOTE: We don't create a new button because we want to reuse the hotkey from the menu items
@@ -71,7 +71,7 @@ export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdo
{...buttonProps} {...buttonProps}
> >
<EnvironmentColorIndicator environment={activeEnvironment ?? null} /> <EnvironmentColorIndicator environment={activeEnvironment ?? null} />
{activeEnvironment?.name ?? (hasBaseVars ? 'Environment' : 'No Environment')} {activeEnvironment?.name ?? (hasBaseVars ? "Environment" : "No Environment")}
</Button> </Button>
</Dropdown> </Dropdown>
); );

View File

@@ -1,6 +1,6 @@
import type { Environment } from '@yaakapp-internal/models'; import type { Environment } from "@yaakapp-internal/models";
import { showColorPicker } from '../lib/showColorPicker'; import { showColorPicker } from "../lib/showColorPicker";
import { ColorIndicator } from './ColorIndicator'; import { ColorIndicator } from "./ColorIndicator";
export function EnvironmentColorIndicator({ export function EnvironmentColorIndicator({
environment, environment,

View File

@@ -1,8 +1,8 @@
import { useState } from 'react'; import { useState } from "react";
import { ColorIndicator } from './ColorIndicator'; import { ColorIndicator } from "./ColorIndicator";
import { Banner } from '@yaakapp-internal/ui'; import { Banner } from "@yaakapp-internal/ui";
import { Button } from './core/Button'; import { Button } from "./core/Button";
import { ColorPickerWithThemeColors } from './core/ColorPicker'; import { ColorPickerWithThemeColors } from "./core/ColorPicker";
export function EnvironmentColorPicker({ export function EnvironmentColorPicker({
color: defaultColor, color: defaultColor,

View File

@@ -1,34 +1,34 @@
import type { Environment, Workspace } from '@yaakapp-internal/models'; import type { Environment, Workspace } from "@yaakapp-internal/models";
import { duplicateModel, patchModel } from '@yaakapp-internal/models'; import { duplicateModel, patchModel } from "@yaakapp-internal/models";
import type { TreeHandle, TreeNode, TreeProps } from '@yaakapp-internal/ui'; import type { TreeHandle, TreeNode, TreeProps } from "@yaakapp-internal/ui";
import { Banner, Icon, InlineCode, SplitLayout, Tree } from '@yaakapp-internal/ui'; import { Banner, Icon, InlineCode, SplitLayout, Tree } from "@yaakapp-internal/ui";
import { atom, useAtomValue } from 'jotai'; import { atom, useAtomValue } from "jotai";
import { atomFamily } from 'jotai/utils'; import { atomFamily } from "jotai/utils";
import { useCallback, useLayoutEffect, useRef, useState } from 'react'; import { useCallback, useLayoutEffect, useRef, useState } from "react";
import { createSubEnvironmentAndActivate } from '../commands/createEnvironment'; import { createSubEnvironmentAndActivate } from "../commands/createEnvironment";
import { activeWorkspaceAtom, activeWorkspaceIdAtom } from '../hooks/useActiveWorkspace'; import { activeWorkspaceAtom, activeWorkspaceIdAtom } from "../hooks/useActiveWorkspace";
import { import {
environmentsBreakdownAtom, environmentsBreakdownAtom,
useEnvironmentsBreakdown, useEnvironmentsBreakdown,
} from '../hooks/useEnvironmentsBreakdown'; } from "../hooks/useEnvironmentsBreakdown";
import { useHotKey } from '../hooks/useHotKey'; import { useHotKey } from "../hooks/useHotKey";
import { atomWithKVStorage } from '../lib/atoms/atomWithKVStorage'; import { atomWithKVStorage } from "../lib/atoms/atomWithKVStorage";
import { deleteModelWithConfirm } from '../lib/deleteModelWithConfirm'; import { deleteModelWithConfirm } from "../lib/deleteModelWithConfirm";
import { jotaiStore } from '../lib/jotai'; import { jotaiStore } from "../lib/jotai";
import { isBaseEnvironment, isSubEnvironment } from '../lib/model_util'; import { isBaseEnvironment, isSubEnvironment } from "../lib/model_util";
import { resolvedModelName } from '../lib/resolvedModelName'; import { resolvedModelName } from "../lib/resolvedModelName";
import { showColorPicker } from '../lib/showColorPicker'; import { showColorPicker } from "../lib/showColorPicker";
import type { ContextMenuProps, DropdownItem } from './core/Dropdown'; import type { ContextMenuProps, DropdownItem } from "./core/Dropdown";
import { ContextMenu } from './core/Dropdown'; import { ContextMenu } from "./core/Dropdown";
import { IconButton } from './core/IconButton'; import { IconButton } from "./core/IconButton";
import { IconTooltip } from './core/IconTooltip'; import { IconTooltip } from "./core/IconTooltip";
import type { PairEditorHandle } from './core/PairEditor'; import type { PairEditorHandle } from "./core/PairEditor";
import { EnvironmentColorIndicator } from './EnvironmentColorIndicator'; import { EnvironmentColorIndicator } from "./EnvironmentColorIndicator";
import { EnvironmentEditor } from './EnvironmentEditor'; import { EnvironmentEditor } from "./EnvironmentEditor";
import { EnvironmentSharableTooltip } from './EnvironmentSharableTooltip'; import { EnvironmentSharableTooltip } from "./EnvironmentSharableTooltip";
const collapsedFamily = atomFamily((treeId: string) => { const collapsedFamily = atomFamily((treeId: string) => {
const key = ['env_collapsed', treeId ?? 'n/a']; const key = ["env_collapsed", treeId ?? "n/a"];
return atomWithKVStorage<Record<string, boolean>>(key, {}); return atomWithKVStorage<Record<string, boolean>>(key, {});
}); });
@@ -111,7 +111,7 @@ function EnvironmentEditDialogSidebar({
selectedEnvironmentId: string | null; selectedEnvironmentId: string | null;
setSelectedEnvironmentId: (id: string | null) => void; setSelectedEnvironmentId: (id: string | null) => void;
}) { }) {
const activeWorkspaceId = useAtomValue(activeWorkspaceIdAtom) ?? ''; const activeWorkspaceId = useAtomValue(activeWorkspaceIdAtom) ?? "";
const treeId = `environment.${activeWorkspaceId}.sidebar`; const treeId = `environment.${activeWorkspaceId}.sidebar`;
const treeRef = useRef<TreeHandle>(null); const treeRef = useRef<TreeHandle>(null);
const { baseEnvironment, baseEnvironments } = useEnvironmentsBreakdown(); const { baseEnvironment, baseEnvironments } = useEnvironmentsBreakdown();
@@ -164,13 +164,13 @@ function EnvironmentEditDialogSidebar({
[setSelectedEnvironmentId], [setSelectedEnvironmentId],
); );
useHotKey('sidebar.selected.rename', handleRenameSelected, { useHotKey("sidebar.selected.rename", handleRenameSelected, {
enable: treeHasFocus, enable: treeHasFocus,
allowDefault: true, allowDefault: true,
priority: 100, priority: 100,
}); });
useHotKey( useHotKey(
'sidebar.selected.delete', "sidebar.selected.delete",
useCallback(() => { useCallback(() => {
const items = getSelectedTreeModels(); const items = getSelectedTreeModels();
if (items) handleDeleteSelected(items); if (items) handleDeleteSelected(items);
@@ -178,7 +178,7 @@ function EnvironmentEditDialogSidebar({
{ enable: treeHasFocus, priority: 100 }, { enable: treeHasFocus, priority: 100 },
); );
useHotKey( useHotKey(
'sidebar.selected.duplicate', "sidebar.selected.duplicate",
useCallback(async () => { useCallback(async () => {
const items = getSelectedTreeModels(); const items = getSelectedTreeModels();
if (items) await handleDuplicateSelected(items); if (items) await handleDuplicateSelected(items);
@@ -187,17 +187,17 @@ function EnvironmentEditDialogSidebar({
); );
const getContextMenu = useCallback( const getContextMenu = useCallback(
(items: TreeModel[]): ContextMenuProps['items'] => { (items: TreeModel[]): ContextMenuProps["items"] => {
const environment = items[0]; const environment = items[0];
const addEnvironmentItem: DropdownItem = { const addEnvironmentItem: DropdownItem = {
label: 'Create Sub Environment', label: "Create Sub Environment",
leftSlot: <Icon icon="plus" />, leftSlot: <Icon icon="plus" />,
onSelect: async () => { onSelect: async () => {
await createSubEnvironment(); await createSubEnvironment();
}, },
}; };
if (environment == null || environment.model !== 'environment') { if (environment == null || environment.model !== "environment") {
return [addEnvironmentItem]; return [addEnvironmentItem];
} }
@@ -208,10 +208,10 @@ function EnvironmentEditDialogSidebar({
const menuItems: DropdownItem[] = [ const menuItems: DropdownItem[] = [
{ {
label: 'Rename', label: "Rename",
leftSlot: <Icon icon="pencil" />, leftSlot: <Icon icon="pencil" />,
hidden: isBaseEnvironment(environment) || !singleEnvironment, hidden: isBaseEnvironment(environment) || !singleEnvironment,
hotKeyAction: 'sidebar.selected.rename', hotKeyAction: "sidebar.selected.rename",
hotKeyLabelOnly: true, hotKeyLabelOnly: true,
onSelect: () => { onSelect: () => {
// Not sure why this is needed, but without it the // Not sure why this is needed, but without it the
@@ -220,22 +220,22 @@ function EnvironmentEditDialogSidebar({
}, },
}, },
{ {
label: 'Duplicate', label: "Duplicate",
leftSlot: <Icon icon="copy" />, leftSlot: <Icon icon="copy" />,
hidden: isBaseEnvironment(environment), hidden: isBaseEnvironment(environment),
hotKeyAction: 'sidebar.selected.duplicate', hotKeyAction: "sidebar.selected.duplicate",
hotKeyLabelOnly: true, hotKeyLabelOnly: true,
onSelect: () => handleDuplicateSelected(items), onSelect: () => handleDuplicateSelected(items),
}, },
{ {
label: environment.color ? 'Change Color' : 'Assign Color', label: environment.color ? "Change Color" : "Assign Color",
leftSlot: <Icon icon="palette" />, leftSlot: <Icon icon="palette" />,
hidden: isBaseEnvironment(environment) || !singleEnvironment, hidden: isBaseEnvironment(environment) || !singleEnvironment,
onSelect: async () => showColorPicker(environment), onSelect: async () => showColorPicker(environment),
}, },
{ {
label: `Make ${environment.public ? 'Private' : 'Sharable'}`, label: `Make ${environment.public ? "Private" : "Sharable"}`,
leftSlot: <Icon icon={environment.public ? 'eye_closed' : 'eye'} />, leftSlot: <Icon icon={environment.public ? "eye_closed" : "eye"} />,
rightSlot: <EnvironmentSharableTooltip />, rightSlot: <EnvironmentSharableTooltip />,
hidden: items.length > 1, hidden: items.length > 1,
onSelect: async () => { onSelect: async () => {
@@ -243,9 +243,9 @@ function EnvironmentEditDialogSidebar({
}, },
}, },
{ {
color: 'danger', color: "danger",
label: 'Delete', label: "Delete",
hotKeyAction: 'sidebar.selected.delete', hotKeyAction: "sidebar.selected.delete",
hotKeyLabelOnly: true, hotKeyLabelOnly: true,
hidden: !canDeleteEnvironment, hidden: !canDeleteEnvironment,
leftSlot: <Icon icon="trash" />, leftSlot: <Icon icon="trash" />,
@@ -255,7 +255,7 @@ function EnvironmentEditDialogSidebar({
// Add sub environment to base environment // Add sub environment to base environment
if (isBaseEnvironment(environment) && singleEnvironment) { if (isBaseEnvironment(environment) && singleEnvironment) {
menuItems.push({ type: 'separator' }); menuItems.push({ type: "separator" });
menuItems.push(addEnvironmentItem); menuItems.push(addEnvironmentItem);
} }
@@ -313,7 +313,7 @@ function EnvironmentEditDialogSidebar({
[setSelectedEnvironmentId], [setSelectedEnvironmentId],
); );
const renderContextMenuFn = useCallback<NonNullable<TreeProps<TreeModel>['renderContextMenu']>>( const renderContextMenuFn = useCallback<NonNullable<TreeProps<TreeModel>["renderContextMenu"]>>(
({ items, position, onClose }) => ( ({ items, position, onClose }) => (
<ContextMenu items={items as DropdownItem[]} triggerPosition={position} onClose={onClose} /> <ContextMenu items={items as DropdownItem[]} triggerPosition={position} onClose={onClose} />
), ),
@@ -386,7 +386,7 @@ function ItemLeftSlotInner({ item }: { item: TreeModel }) {
return baseEnvironments.length > 1 ? ( return baseEnvironments.length > 1 ? (
<Icon icon="alert_triangle" color="notice" /> <Icon icon="alert_triangle" color="notice" />
) : ( ) : (
item.model === 'environment' && item.color && <EnvironmentColorIndicator environment={item} /> item.model === "environment" && item.color && <EnvironmentColorIndicator environment={item} />
); );
} }
@@ -394,7 +394,7 @@ function ItemRightSlot({ item }: { item: TreeModel }) {
const { baseEnvironments } = useEnvironmentsBreakdown(); const { baseEnvironments } = useEnvironmentsBreakdown();
return ( return (
<> <>
{item.model === 'environment' && baseEnvironments.length <= 1 && isBaseEnvironment(item) && ( {item.model === "environment" && baseEnvironments.length <= 1 && isBaseEnvironment(item) && (
<IconButton <IconButton
size="sm" size="sm"
color="custom" color="custom"
@@ -412,7 +412,7 @@ function ItemRightSlot({ item }: { item: TreeModel }) {
function ItemInner({ item }: { item: TreeModel }) { function ItemInner({ item }: { item: TreeModel }) {
return ( return (
<div className="grid grid-cols-[auto_minmax(0,1fr)] w-full items-center"> <div className="grid grid-cols-[auto_minmax(0,1fr)] w-full items-center">
{item.model === 'environment' && item.public ? ( {item.model === "environment" && item.public ? (
<div className="mr-2 flex items-center">{sharableTooltip}</div> <div className="mr-2 flex items-center">{sharableTooltip}</div>
) : ( ) : (
<span aria-hidden /> <span aria-hidden />
@@ -430,9 +430,9 @@ async function createSubEnvironment() {
} }
function getEditOptions(item: TreeModel) { function getEditOptions(item: TreeModel) {
const options: ReturnType<NonNullable<TreeProps<TreeModel>['getEditOptions']>> = { const options: ReturnType<NonNullable<TreeProps<TreeModel>["getEditOptions"]>> = {
defaultValue: item.name, defaultValue: item.name,
placeholder: 'Name', placeholder: "Name",
async onChange(item, name) { async onChange(item, name) {
await patchModel(item, { name }); await patchModel(item, { name });
}, },

View File

@@ -1,27 +1,27 @@
import type { Environment } from '@yaakapp-internal/models'; import type { Environment } from "@yaakapp-internal/models";
import { patchModel } from '@yaakapp-internal/models'; import { patchModel } from "@yaakapp-internal/models";
import type { GenericCompletionOption } from '@yaakapp-internal/plugins'; import type { GenericCompletionOption } from "@yaakapp-internal/plugins";
import { Heading } from '@yaakapp-internal/ui'; import { Heading } from "@yaakapp-internal/ui";
import classNames from 'classnames'; import classNames from "classnames";
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from "react";
import { useEnvironmentsBreakdown } from '../hooks/useEnvironmentsBreakdown'; import { useEnvironmentsBreakdown } from "../hooks/useEnvironmentsBreakdown";
import { useIsEncryptionEnabled } from '../hooks/useIsEncryptionEnabled'; import { useIsEncryptionEnabled } from "../hooks/useIsEncryptionEnabled";
import { useKeyValue } from '../hooks/useKeyValue'; import { useKeyValue } from "../hooks/useKeyValue";
import { useRandomKey } from '../hooks/useRandomKey'; import { useRandomKey } from "../hooks/useRandomKey";
import { analyzeTemplate, convertTemplateToSecure } from '../lib/encryption'; import { analyzeTemplate, convertTemplateToSecure } from "../lib/encryption";
import { isBaseEnvironment } from '../lib/model_util'; import { isBaseEnvironment } from "../lib/model_util";
import { import {
setupOrConfigureEncryption, setupOrConfigureEncryption,
withEncryptionEnabled, withEncryptionEnabled,
} from '../lib/setupOrConfigureEncryption'; } from "../lib/setupOrConfigureEncryption";
import { DismissibleBanner } from './core/DismissibleBanner'; import { DismissibleBanner } from "./core/DismissibleBanner";
import type { GenericCompletionConfig } from './core/Editor/genericCompletion'; import type { GenericCompletionConfig } from "./core/Editor/genericCompletion";
import type { PairEditorHandle, PairWithId } from './core/PairEditor'; import type { PairEditorHandle, PairWithId } from "./core/PairEditor";
import { ensurePairId } from './core/PairEditor.util'; import { ensurePairId } from "./core/PairEditor.util";
import { PairOrBulkEditor } from './core/PairOrBulkEditor'; import { PairOrBulkEditor } from "./core/PairOrBulkEditor";
import { PillButton } from './core/PillButton'; import { PillButton } from "./core/PillButton";
import { EnvironmentColorIndicator } from './EnvironmentColorIndicator'; import { EnvironmentColorIndicator } from "./EnvironmentColorIndicator";
import { EnvironmentSharableTooltip } from './EnvironmentSharableTooltip'; import { EnvironmentSharableTooltip } from "./EnvironmentSharableTooltip";
interface Props { interface Props {
environment: Environment; environment: Environment;
@@ -34,8 +34,8 @@ export function EnvironmentEditor({ environment, hideName, className, setRef }:
const workspaceId = environment.workspaceId; const workspaceId = environment.workspaceId;
const isEncryptionEnabled = useIsEncryptionEnabled(); const isEncryptionEnabled = useIsEncryptionEnabled();
const valueVisibility = useKeyValue<boolean>({ const valueVisibility = useKeyValue<boolean>({
namespace: 'global', namespace: "global",
key: ['environmentValueVisibility', workspaceId], key: ["environmentValueVisibility", workspaceId],
fallback: false, fallback: false,
}); });
const { allEnvironments } = useEnvironmentsBreakdown(); const { allEnvironments } = useEnvironmentsBreakdown();
@@ -64,8 +64,8 @@ export function EnvironmentEditor({ environment, hideName, className, setRef }:
} }
options.push({ options.push({
label: name, label: name,
type: 'constant', type: "constant",
detail: containingEnvs.map((e) => e.name).join(', '), detail: containingEnvs.map((e) => e.name).join(", "),
}); });
} }
return { options }; return { options };
@@ -73,14 +73,14 @@ export function EnvironmentEditor({ environment, hideName, className, setRef }:
const validateName = useCallback((name: string) => { const validateName = useCallback((name: string) => {
// Empty just means the variable doesn't have a name yet and is unusable // Empty just means the variable doesn't have a name yet and is unusable
if (name === '') return true; if (name === "") return true;
return name.match(/^[a-z_][a-z0-9_.-]*$/i) != null; return name.match(/^[a-z_][a-z0-9_.-]*$/i) != null;
}, []); }, []);
const valueType = !isEncryptionEnabled && valueVisibility.value ? 'text' : 'password'; const valueType = !isEncryptionEnabled && valueVisibility.value ? "text" : "password";
const allVariableAreEncrypted = useMemo( const allVariableAreEncrypted = useMemo(
() => () =>
environment.variables.every((v) => v.value === '' || analyzeTemplate(v.value) !== 'insecure'), environment.variables.every((v) => v.value === "" || analyzeTemplate(v.value) !== "insecure"),
[environment.variables], [environment.variables],
); );
@@ -88,7 +88,7 @@ export function EnvironmentEditor({ environment, hideName, className, setRef }:
withEncryptionEnabled(async () => { withEncryptionEnabled(async () => {
const encryptedVariables: PairWithId[] = []; const encryptedVariables: PairWithId[] = [];
for (const variable of environment.variables) { for (const variable of environment.variables) {
const value = variable.value ? await convertTemplateToSecure(variable.value) : ''; const value = variable.value ? await convertTemplateToSecure(variable.value) : "";
encryptedVariables.push(ensurePairId({ ...variable, value })); encryptedVariables.push(ensurePairId({ ...variable, value }));
} }
await handleChange(encryptedVariables); await handleChange(encryptedVariables);
@@ -100,7 +100,7 @@ export function EnvironmentEditor({ environment, hideName, className, setRef }:
<div <div
className={classNames( className={classNames(
className, className,
'h-full grid grid-rows-[auto_minmax(0,1fr)] gap-2 pr-3 pb-3', "h-full grid grid-rows-[auto_minmax(0,1fr)] gap-2 pr-3 pb-3",
)} )}
> >
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
@@ -123,7 +123,7 @@ export function EnvironmentEditor({ environment, hideName, className, setRef }:
) )
) : ( ) : (
<PillButton color="secondary" onClick={() => valueVisibility.set((v) => !v)}> <PillButton color="secondary" onClick={() => valueVisibility.set((v) => !v)}>
{valueVisibility.value ? 'Hide Values' : 'Show Values'} {valueVisibility.value ? "Hide Values" : "Show Values"}
</PillButton> </PillButton>
)} )}
<PillButton <PillButton
@@ -133,7 +133,7 @@ export function EnvironmentEditor({ environment, hideName, className, setRef }:
await patchModel(environment, { public: !environment.public }); await patchModel(environment, { public: !environment.public });
}} }}
> >
{environment.public ? 'Sharable' : 'Private'} {environment.public ? "Sharable" : "Private"}
</PillButton> </PillButton>
</Heading> </Heading>
{environment.public && (!isEncryptionEnabled || !allVariableAreEncrypted) && ( {environment.public && (!isEncryptionEnabled || !allVariableAreEncrypted) && (
@@ -143,9 +143,9 @@ export function EnvironmentEditor({ environment, hideName, className, setRef }:
className="mr-3" className="mr-3"
actions={[ actions={[
{ {
label: 'Encrypt Variables', label: "Encrypt Variables",
onClick: () => encryptEnvironment(environment), onClick: () => encryptEnvironment(environment),
color: 'success', color: "success",
}, },
]} ]}
> >

View File

@@ -1,4 +1,4 @@
import { IconTooltip } from './core/IconTooltip'; import { IconTooltip } from "./core/IconTooltip";
export function EnvironmentSharableTooltip() { export function EnvironmentSharableTooltip() {
return ( return (

View File

@@ -1,8 +1,8 @@
import { Banner, Button, InlineCode } from '@yaakapp-internal/ui'; import { Banner, Button, InlineCode } from "@yaakapp-internal/ui";
import type { ErrorInfo, ReactNode } from 'react'; import type { ErrorInfo, ReactNode } from "react";
import { Component, useEffect } from 'react'; import { Component, useEffect } from "react";
import { showDialog } from '../lib/dialog'; import { showDialog } from "../lib/dialog";
import RouteError from './RouteError'; import RouteError from "./RouteError";
interface ErrorBoundaryProps { interface ErrorBoundaryProps {
name: string; name: string;
@@ -25,7 +25,7 @@ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundarySt
} }
componentDidCatch(error: Error, info: ErrorInfo) { componentDidCatch(error: Error, info: ErrorInfo) {
console.warn('Error caught by ErrorBoundary:', error, info); console.warn("Error caught by ErrorBoundary:", error, info);
} }
render() { render() {
@@ -42,7 +42,7 @@ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundarySt
size="2xs" size="2xs"
onClick={() => { onClick={() => {
showDialog({ showDialog({
id: 'error-boundary', id: "error-boundary",
render: () => <RouteError error={this.state.error} />, render: () => <RouteError error={this.state.error} />,
}); });
}} }}
@@ -59,7 +59,7 @@ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundarySt
export function ErrorBoundaryTestThrow() { export function ErrorBoundaryTestThrow() {
useEffect(() => { useEffect(() => {
throw new Error('test error'); throw new Error("test error");
}); });
return <div>Hello</div>; return <div>Hello</div>;

View File

@@ -1,17 +1,17 @@
import { save } from '@tauri-apps/plugin-dialog'; import { save } from "@tauri-apps/plugin-dialog";
import type { Workspace } from '@yaakapp-internal/models'; import type { Workspace } from "@yaakapp-internal/models";
import { workspacesAtom } from '@yaakapp-internal/models'; import { workspacesAtom } from "@yaakapp-internal/models";
import { HStack, VStack } from '@yaakapp-internal/ui'; import { HStack, VStack } from "@yaakapp-internal/ui";
import { useAtomValue } from 'jotai'; import { useAtomValue } from "jotai";
import { useCallback, useMemo, useState } from 'react'; import { useCallback, useMemo, useState } from "react";
import slugify from 'slugify'; import slugify from "slugify";
import { activeWorkspaceAtom } from '../hooks/useActiveWorkspace'; import { activeWorkspaceAtom } from "../hooks/useActiveWorkspace";
import { pluralizeCount } from '../lib/pluralize'; import { pluralizeCount } from "../lib/pluralize";
import { invokeCmd } from '../lib/tauri'; import { invokeCmd } from "../lib/tauri";
import { Button } from './core/Button'; import { Button } from "./core/Button";
import { Checkbox } from './core/Checkbox'; import { Checkbox } from "./core/Checkbox";
import { DetailsBanner } from './core/DetailsBanner'; import { DetailsBanner } from "./core/DetailsBanner";
import { Link } from './core/Link'; import { Link } from "./core/Link";
interface Props { interface Props {
onHide: () => void; onHide: () => void;
@@ -63,16 +63,16 @@ function ExportDataDialogContent({
const handleExport = useCallback(async () => { const handleExport = useCallback(async () => {
const ids = Object.keys(selectedWorkspaces).filter((k) => selectedWorkspaces[k]); const ids = Object.keys(selectedWorkspaces).filter((k) => selectedWorkspaces[k]);
const workspace = ids.length === 1 ? workspaces.find((w) => w.id === ids[0]) : undefined; const workspace = ids.length === 1 ? workspaces.find((w) => w.id === ids[0]) : undefined;
const slug = workspace ? slugify(workspace.name, { lower: true }) : 'workspaces'; const slug = workspace ? slugify(workspace.name, { lower: true }) : "workspaces";
const exportPath = await save({ const exportPath = await save({
title: 'Export Data', title: "Export Data",
defaultPath: `yaak.${slug}.json`, defaultPath: `yaak.${slug}.json`,
}); });
if (exportPath == null) { if (exportPath == null) {
return; return;
} }
await invokeCmd('cmd_export_data', { await invokeCmd("cmd_export_data", {
workspaceIds: ids, workspaceIds: ids,
exportPath, exportPath,
includePrivateEnvironments: includePrivateEnvironments, includePrivateEnvironments: includePrivateEnvironments,
@@ -92,7 +92,7 @@ function ExportDataDialogContent({
<tr> <tr>
<th className="w-6 min-w-0 py-2 text-left pl-1"> <th className="w-6 min-w-0 py-2 text-left pl-1">
<Checkbox <Checkbox
checked={!allSelected && !noneSelected ? 'indeterminate' : allSelected} checked={!allSelected && !noneSelected ? "indeterminate" : allSelected}
hideLabel hideLabel
title="All workspaces" title="All workspaces"
onChange={handleToggleAll} onChange={handleToggleAll}
@@ -122,7 +122,7 @@ function ExportDataDialogContent({
setSelectedWorkspaces((prev) => ({ ...prev, [w.id]: !prev[w.id] })) setSelectedWorkspaces((prev) => ({ ...prev, [w.id]: !prev[w.id] }))
} }
> >
{w.name} {w.id === activeWorkspace.id ? '(current workspace)' : ''} {w.name} {w.id === activeWorkspace.id ? "(current workspace)" : ""}
</td> </td>
</tr> </tr>
))} ))}
@@ -155,8 +155,8 @@ function ExportDataDialogContent({
disabled={noneSelected} disabled={noneSelected}
onClick={() => handleExport()} onClick={() => handleExport()}
> >
Export{' '} Export{" "}
{pluralizeCount('Workspace', numSelected, { omitSingle: true, noneWord: 'Nothing' })} {pluralizeCount("Workspace", numSelected, { omitSingle: true, noneWord: "Nothing" })}
</Button> </Button>
</HStack> </HStack>
</footer> </footer>

View File

@@ -1,24 +1,24 @@
import type { Folder, GrpcRequest, HttpRequest, WebsocketRequest } from '@yaakapp-internal/models'; import type { Folder, GrpcRequest, HttpRequest, WebsocketRequest } from "@yaakapp-internal/models";
import { foldersAtom } from '@yaakapp-internal/models'; import { foldersAtom } from "@yaakapp-internal/models";
import { Heading, HStack, Icon, LoadingIcon } from '@yaakapp-internal/ui'; import { Heading, HStack, Icon, LoadingIcon } from "@yaakapp-internal/ui";
import classNames from 'classnames'; import classNames from "classnames";
import { useAtomValue } from 'jotai'; import { useAtomValue } from "jotai";
import type { CSSProperties, ReactNode } from 'react'; import type { CSSProperties, ReactNode } from "react";
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from "react";
import { allRequestsAtom } from '../hooks/useAllRequests'; import { allRequestsAtom } from "../hooks/useAllRequests";
import { useFolderActions } from '../hooks/useFolderActions'; import { useFolderActions } from "../hooks/useFolderActions";
import { useLatestHttpResponse } from '../hooks/useLatestHttpResponse'; import { useLatestHttpResponse } from "../hooks/useLatestHttpResponse";
import { sendAnyHttpRequest } from '../hooks/useSendAnyHttpRequest'; import { sendAnyHttpRequest } from "../hooks/useSendAnyHttpRequest";
import { showDialog } from '../lib/dialog'; import { showDialog } from "../lib/dialog";
import { resolvedModelName } from '../lib/resolvedModelName'; import { resolvedModelName } from "../lib/resolvedModelName";
import { router } from '../lib/router'; import { router } from "../lib/router";
import { Button } from './core/Button'; import { Button } from "./core/Button";
import { HttpResponseDurationTag } from './core/HttpResponseDurationTag'; import { HttpResponseDurationTag } from "./core/HttpResponseDurationTag";
import { HttpStatusTag } from './core/HttpStatusTag'; import { HttpStatusTag } from "./core/HttpStatusTag";
import { IconButton } from './core/IconButton'; import { IconButton } from "./core/IconButton";
import { Separator } from './core/Separator'; import { Separator } from "./core/Separator";
import { SizeTag } from './core/SizeTag'; import { SizeTag } from "./core/SizeTag";
import { HttpResponsePane } from './HttpResponsePane'; import { HttpResponsePane } from "./HttpResponsePane";
interface Props { interface Props {
folder: Folder; folder: Folder;
@@ -30,7 +30,7 @@ export function FolderLayout({ folder, style }: Props) {
const requests = useAtomValue(allRequestsAtom); const requests = useAtomValue(allRequestsAtom);
const folderActions = useFolderActions(); const folderActions = useFolderActions();
const sendAllAction = useMemo( const sendAllAction = useMemo(
() => folderActions.find((a) => a.label === 'Send All'), () => folderActions.find((a) => a.label === "Send All"),
[folderActions], [folderActions],
); );
@@ -75,13 +75,13 @@ export function FolderLayout({ folder, style }: Props) {
function ChildCard({ child }: { child: Folder | HttpRequest | GrpcRequest | WebsocketRequest }) { function ChildCard({ child }: { child: Folder | HttpRequest | GrpcRequest | WebsocketRequest }) {
let card: ReactNode; let card: ReactNode;
if (child.model === 'folder') { if (child.model === "folder") {
card = <FolderCard folder={child} />; card = <FolderCard folder={child} />;
} else if (child.model === 'http_request') { } else if (child.model === "http_request") {
card = <HttpRequestCard request={child} />; card = <HttpRequestCard request={child} />;
} else if (child.model === 'grpc_request') { } else if (child.model === "grpc_request") {
card = <RequestCard request={child} />; card = <RequestCard request={child} />;
} else if (child.model === 'websocket_request') { } else if (child.model === "websocket_request") {
card = <RequestCard request={child} />; card = <RequestCard request={child} />;
} else { } else {
card = <div>Unknown model</div>; card = <div>Unknown model</div>;
@@ -89,7 +89,7 @@ function ChildCard({ child }: { child: Folder | HttpRequest | GrpcRequest | Webs
const navigate = useCallback(async () => { const navigate = useCallback(async () => {
await router.navigate({ await router.navigate({
to: '/workspaces/$workspaceId', to: "/workspaces/$workspaceId",
params: { workspaceId: child.workspaceId }, params: { workspaceId: child.workspaceId },
search: (prev) => ({ ...prev, request_id: child.id }), search: (prev) => ({ ...prev, request_id: child.id }),
}); });
@@ -98,12 +98,12 @@ function ChildCard({ child }: { child: Folder | HttpRequest | GrpcRequest | Webs
return ( return (
<div <div
className={classNames( className={classNames(
'rounded-lg bg-surface-highlight p-3 pt-1 border border-border', "rounded-lg bg-surface-highlight p-3 pt-1 border border-border",
'flex flex-col gap-3', "flex flex-col gap-3",
)} )}
> >
<HStack space={2}> <HStack space={2}>
{child.model === 'folder' && <Icon icon="folder" size="lg" />} {child.model === "folder" && <Icon icon="folder" size="lg" />}
<Heading className="truncate" level={2}> <Heading className="truncate" level={2}>
{resolvedModelName(child)} {resolvedModelName(child)}
</Heading> </Heading>
@@ -140,7 +140,7 @@ function FolderCard({ folder }: { folder: Folder }) {
color="primary" color="primary"
onClick={async () => { onClick={async () => {
await router.navigate({ await router.navigate({
to: '/workspaces/$workspaceId', to: "/workspaces/$workspaceId",
params: { workspaceId: folder.workspaceId }, params: { workspaceId: folder.workspaceId },
search: (prev) => { search: (prev) => {
return { ...prev, request_id: null, folder_id: folder.id }; return { ...prev, request_id: null, folder_id: folder.id };
@@ -174,10 +174,10 @@ function HttpRequestCard({ request }: { request: HttpRequest }) {
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
showDialog({ showDialog({
id: 'response-preview', id: "response-preview",
title: 'Response Preview', title: "Response Preview",
size: 'md', size: "md",
className: 'h-full', className: "h-full",
render: () => { render: () => {
return <HttpResponsePane activeRequestId={request.id} />; return <HttpResponsePane activeRequestId={request.id} />;
}, },
@@ -188,12 +188,12 @@ function HttpRequestCard({ request }: { request: HttpRequest }) {
space={2} space={2}
alignItems="center" alignItems="center"
className={classNames( className={classNames(
'cursor-default select-none', "cursor-default select-none",
'whitespace-nowrap w-full pl-3 overflow-x-auto font-mono text-sm hide-scrollbars', "whitespace-nowrap w-full pl-3 overflow-x-auto font-mono text-sm hide-scrollbars",
'font-mono text-editor border rounded px-1.5 py-0.5 truncate w-full', "font-mono text-editor border rounded px-1.5 py-0.5 truncate w-full",
)} )}
> >
{latestResponse.state !== 'closed' && <LoadingIcon size="sm" />} {latestResponse.state !== "closed" && <LoadingIcon size="sm" />}
<HttpStatusTag showReason response={latestResponse} /> <HttpStatusTag showReason response={latestResponse} />
<span>&bull;</span> <span>&bull;</span>
<HttpResponseDurationTag response={latestResponse} /> <HttpResponseDurationTag response={latestResponse} />

View File

@@ -1,36 +1,36 @@
import { createWorkspaceModel, foldersAtom, patchModel } from '@yaakapp-internal/models'; import { createWorkspaceModel, foldersAtom, patchModel } from "@yaakapp-internal/models";
import { HStack, Icon, InlineCode, VStack } from '@yaakapp-internal/ui'; import { HStack, Icon, InlineCode, VStack } from "@yaakapp-internal/ui";
import { useAtomValue } from 'jotai'; import { useAtomValue } from "jotai";
import { Fragment, useMemo } from 'react'; import { Fragment, useMemo } from "react";
import { useAuthTab } from '../hooks/useAuthTab'; import { useAuthTab } from "../hooks/useAuthTab";
import { useEnvironmentsBreakdown } from '../hooks/useEnvironmentsBreakdown'; import { useEnvironmentsBreakdown } from "../hooks/useEnvironmentsBreakdown";
import { useHeadersTab } from '../hooks/useHeadersTab'; import { useHeadersTab } from "../hooks/useHeadersTab";
import { useInheritedHeaders } from '../hooks/useInheritedHeaders'; import { useInheritedHeaders } from "../hooks/useInheritedHeaders";
import { useModelAncestors } from '../hooks/useModelAncestors'; import { useModelAncestors } from "../hooks/useModelAncestors";
import { deleteModelWithConfirm } from '../lib/deleteModelWithConfirm'; import { deleteModelWithConfirm } from "../lib/deleteModelWithConfirm";
import { hideDialog } from '../lib/dialog'; import { hideDialog } from "../lib/dialog";
import { CopyIconButton } from './CopyIconButton'; import { CopyIconButton } from "./CopyIconButton";
import { Button } from './core/Button'; import { Button } from "./core/Button";
import { CountBadge } from './core/CountBadge'; import { CountBadge } from "./core/CountBadge";
import { Input } from './core/Input'; import { Input } from "./core/Input";
import { Link } from './core/Link'; import { Link } from "./core/Link";
import type { TabItem } from './core/Tabs/Tabs'; import type { TabItem } from "./core/Tabs/Tabs";
import { TabContent, Tabs } from './core/Tabs/Tabs'; import { TabContent, Tabs } from "./core/Tabs/Tabs";
import { EmptyStateText } from './EmptyStateText'; import { EmptyStateText } from "./EmptyStateText";
import { EnvironmentEditor } from './EnvironmentEditor'; import { EnvironmentEditor } from "./EnvironmentEditor";
import { HeadersEditor } from './HeadersEditor'; import { HeadersEditor } from "./HeadersEditor";
import { HttpAuthenticationEditor } from './HttpAuthenticationEditor'; import { HttpAuthenticationEditor } from "./HttpAuthenticationEditor";
import { MarkdownEditor } from './MarkdownEditor'; import { MarkdownEditor } from "./MarkdownEditor";
interface Props { interface Props {
folderId: string | null; folderId: string | null;
tab?: FolderSettingsTab; tab?: FolderSettingsTab;
} }
const TAB_AUTH = 'auth'; const TAB_AUTH = "auth";
const TAB_HEADERS = 'headers'; const TAB_HEADERS = "headers";
const TAB_VARIABLES = 'variables'; const TAB_VARIABLES = "variables";
const TAB_GENERAL = 'general'; const TAB_GENERAL = "general";
export type FolderSettingsTab = export type FolderSettingsTab =
| typeof TAB_AUTH | typeof TAB_AUTH
@@ -48,7 +48,7 @@ export function FolderSettingsDialog({ folderId, tab }: Props) {
const inheritedHeaders = useInheritedHeaders(folder); const inheritedHeaders = useInheritedHeaders(folder);
const environments = useEnvironmentsBreakdown(); const environments = useEnvironmentsBreakdown();
const folderEnvironment = environments.allEnvironments.find( const folderEnvironment = environments.allEnvironments.find(
(e) => e.parentModel === 'folder' && e.parentId === folderId, (e) => e.parentModel === "folder" && e.parentId === folderId,
); );
const numVars = (folderEnvironment?.variables ?? []).filter((v) => v.name).length; const numVars = (folderEnvironment?.variables ?? []).filter((v) => v.name).length;
@@ -58,13 +58,13 @@ export function FolderSettingsDialog({ folderId, tab }: Props) {
return [ return [
{ {
value: TAB_GENERAL, value: TAB_GENERAL,
label: 'General', label: "General",
}, },
...headersTab, ...headersTab,
...authTab, ...authTab,
{ {
value: TAB_VARIABLES, value: TAB_VARIABLES,
label: 'Variables', label: "Variables",
rightSlot: numVars > 0 ? <CountBadge count={numVars} /> : null, rightSlot: numVars > 0 ? <CountBadge count={numVars} /> : null,
}, },
]; ];
@@ -128,7 +128,7 @@ export function FolderSettingsDialog({ folderId, tab }: Props) {
onClick={async () => { onClick={async () => {
const didDelete = await deleteModelWithConfirm(folder); const didDelete = await deleteModelWithConfirm(folder);
if (didDelete) { if (didDelete) {
hideDialog('folder-settings'); hideDialog("folder-settings");
} }
}} }}
color="danger" color="danger"
@@ -164,10 +164,10 @@ export function FolderSettingsDialog({ folderId, tab }: Props) {
<EmptyStateText> <EmptyStateText>
<VStack alignItems="center" space={1.5}> <VStack alignItems="center" space={1.5}>
<p> <p>
Override{' '} Override{" "}
<Link href="https://yaak.app/docs/using-yaak/environments-and-variables"> <Link href="https://yaak.app/docs/using-yaak/environments-and-variables">
Variables Variables
</Link>{' '} </Link>{" "}
for requests within this folder. for requests within this folder.
</p> </p>
<Button <Button
@@ -176,10 +176,10 @@ export function FolderSettingsDialog({ folderId, tab }: Props) {
onClick={async () => { onClick={async () => {
await createWorkspaceModel({ await createWorkspaceModel({
workspaceId: folder.workspaceId, workspaceId: folder.workspaceId,
parentModel: 'folder', parentModel: "folder",
parentId: folder.id, parentId: folder.id,
model: 'environment', model: "environment",
name: 'Folder Environment', name: "Folder Environment",
}); });
}} }}
> >

View File

@@ -1,12 +1,12 @@
import type { HttpRequest } from '@yaakapp-internal/models'; import type { HttpRequest } from "@yaakapp-internal/models";
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from "react";
import type { Pair, PairEditorProps } from './core/PairEditor'; import type { Pair, PairEditorProps } from "./core/PairEditor";
import { PairEditor } from './core/PairEditor'; import { PairEditor } from "./core/PairEditor";
type Props = { type Props = {
forceUpdateKey: string; forceUpdateKey: string;
request: HttpRequest; request: HttpRequest;
onChange: (body: HttpRequest['body']) => void; onChange: (body: HttpRequest["body"]) => void;
}; };
export function FormMultipartEditor({ request, forceUpdateKey, onChange }: Props) { export function FormMultipartEditor({ request, forceUpdateKey, onChange }: Props) {
@@ -24,7 +24,7 @@ export function FormMultipartEditor({ request, forceUpdateKey, onChange }: Props
[request.body.form], [request.body.form],
); );
const handleChange = useCallback<PairEditorProps['onChange']>( const handleChange = useCallback<PairEditorProps["onChange"]>(
(pairs) => (pairs) =>
onChange({ onChange({
form: pairs.map((p) => ({ form: pairs.map((p) => ({

View File

@@ -1,12 +1,12 @@
import type { HttpRequest } from '@yaakapp-internal/models'; import type { HttpRequest } from "@yaakapp-internal/models";
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from "react";
import type { Pair, PairEditorProps } from './core/PairEditor'; import type { Pair, PairEditorProps } from "./core/PairEditor";
import { PairOrBulkEditor } from './core/PairOrBulkEditor'; import { PairOrBulkEditor } from "./core/PairOrBulkEditor";
type Props = { type Props = {
forceUpdateKey: string; forceUpdateKey: string;
request: HttpRequest; request: HttpRequest;
onChange: (headers: HttpRequest['body']) => void; onChange: (headers: HttpRequest["body"]) => void;
}; };
export function FormUrlencodedEditor({ request, forceUpdateKey, onChange }: Props) { export function FormUrlencodedEditor({ request, forceUpdateKey, onChange }: Props) {
@@ -14,14 +14,14 @@ export function FormUrlencodedEditor({ request, forceUpdateKey, onChange }: Prop
() => () =>
(Array.isArray(request.body.form) ? request.body.form : []).map((p) => ({ (Array.isArray(request.body.form) ? request.body.form : []).map((p) => ({
enabled: !!p.enabled, enabled: !!p.enabled,
name: p.name || '', name: p.name || "",
value: p.value || '', value: p.value || "",
id: p.id, id: p.id,
})), })),
[request.body.form], [request.body.form],
); );
const handleChange = useCallback<PairEditorProps['onChange']>( const handleChange = useCallback<PairEditorProps["onChange"]>(
(pairs) => (pairs) =>
onChange({ form: pairs.map((p) => ({ enabled: p.enabled, name: p.name, value: p.value })) }), onChange({ form: pairs.map((p) => ({ enabled: p.enabled, name: p.name, value: p.value })) }),
[onChange], [onChange],

View File

@@ -1,14 +1,14 @@
import { activeRequestAtom } from '../hooks/useActiveRequest'; import { activeRequestAtom } from "../hooks/useActiveRequest";
import { useSubscribeActiveWorkspaceId } from '../hooks/useActiveWorkspace'; import { useSubscribeActiveWorkspaceId } from "../hooks/useActiveWorkspace";
import { useActiveWorkspaceChangedToast } from '../hooks/useActiveWorkspaceChangedToast'; import { useActiveWorkspaceChangedToast } from "../hooks/useActiveWorkspaceChangedToast";
import { useHotKey, useSubscribeHotKeys } from '../hooks/useHotKey'; import { useHotKey, useSubscribeHotKeys } from "../hooks/useHotKey";
import { useSubscribeHttpAuthentication } from '../hooks/useHttpAuthentication'; import { useSubscribeHttpAuthentication } from "../hooks/useHttpAuthentication";
import { useSyncFontSizeSetting } from '../hooks/useSyncFontSizeSetting'; import { useSyncFontSizeSetting } from "../hooks/useSyncFontSizeSetting";
import { useSyncWorkspaceChildModels } from '../hooks/useSyncWorkspaceChildModels'; import { useSyncWorkspaceChildModels } from "../hooks/useSyncWorkspaceChildModels";
import { useSyncZoomSetting } from '../hooks/useSyncZoomSetting'; import { useSyncZoomSetting } from "../hooks/useSyncZoomSetting";
import { useSubscribeTemplateFunctions } from '../hooks/useTemplateFunctions'; import { useSubscribeTemplateFunctions } from "../hooks/useTemplateFunctions";
import { jotaiStore } from '../lib/jotai'; import { jotaiStore } from "../lib/jotai";
import { renameModelWithPrompt } from '../lib/renameModelWithPrompt'; import { renameModelWithPrompt } from "../lib/renameModelWithPrompt";
export function GlobalHooks() { export function GlobalHooks() {
useSyncZoomSetting(); useSyncZoomSetting();
@@ -25,7 +25,7 @@ export function GlobalHooks() {
useSubscribeHotKeys(); useSubscribeHotKeys();
useHotKey( useHotKey(
'request.rename', "request.rename",
async () => { async () => {
const model = jotaiStore.get(activeRequestAtom); const model = jotaiStore.get(activeRequestAtom);
if (model == null) return; if (model == null) return;

View File

@@ -1,18 +1,18 @@
import { patchModel } from '@yaakapp-internal/models'; import { patchModel } from "@yaakapp-internal/models";
import classNames from 'classnames'; import classNames from "classnames";
import { useAtomValue } from 'jotai'; import { useAtomValue } from "jotai";
import type { CSSProperties } from 'react'; import type { CSSProperties } from "react";
import { useEffect, useMemo } from 'react'; import { useEffect, useMemo } from "react";
import { useActiveRequest } from '../hooks/useActiveRequest'; import { useActiveRequest } from "../hooks/useActiveRequest";
import { useGrpc } from '../hooks/useGrpc'; import { useGrpc } from "../hooks/useGrpc";
import { useGrpcProtoFiles } from '../hooks/useGrpcProtoFiles'; import { useGrpcProtoFiles } from "../hooks/useGrpcProtoFiles";
import { activeGrpcConnectionAtom, useGrpcEvents } from '../hooks/usePinnedGrpcConnection'; import { activeGrpcConnectionAtom, useGrpcEvents } from "../hooks/usePinnedGrpcConnection";
import { Banner, SplitLayout } from '@yaakapp-internal/ui'; import { Banner, SplitLayout } from "@yaakapp-internal/ui";
import { activeWorkspaceAtom } from '../hooks/useActiveWorkspace'; import { activeWorkspaceAtom } from "../hooks/useActiveWorkspace";
import { workspaceLayoutAtom } from '../lib/atoms'; import { workspaceLayoutAtom } from "../lib/atoms";
import { HotkeyList } from './core/HotkeyList'; import { HotkeyList } from "./core/HotkeyList";
import { GrpcRequestPane } from './GrpcRequestPane'; import { GrpcRequestPane } from "./GrpcRequestPane";
import { GrpcResponsePane } from './GrpcResponsePane'; import { GrpcResponsePane } from "./GrpcResponsePane";
interface Props { interface Props {
style: CSSProperties; style: CSSProperties;
@@ -23,8 +23,8 @@ const emptyArray: string[] = [];
export function GrpcConnectionLayout({ style }: Props) { export function GrpcConnectionLayout({ style }: Props) {
const workspaceLayout = useAtomValue(workspaceLayoutAtom); const workspaceLayout = useAtomValue(workspaceLayoutAtom);
const activeWorkspace = useAtomValue(activeWorkspaceAtom); const activeWorkspace = useAtomValue(activeWorkspaceAtom);
const wsId = activeWorkspace?.id ?? 'n/a'; const wsId = activeWorkspace?.id ?? "n/a";
const activeRequest = useActiveRequest('grpc_request'); const activeRequest = useActiveRequest("grpc_request");
const activeConnection = useAtomValue(activeGrpcConnectionAtom); const activeConnection = useAtomValue(activeGrpcConnectionAtom);
const grpcEvents = useGrpcEvents(activeConnection?.id ?? null); const grpcEvents = useGrpcEvents(activeConnection?.id ?? null);
const protoFilesKv = useGrpcProtoFiles(activeRequest?.id ?? null); const protoFilesKv = useGrpcProtoFiles(activeRequest?.id ?? null);
@@ -61,18 +61,18 @@ export function GrpcConnectionLayout({ style }: Props) {
}, [activeRequest, services]); }, [activeRequest, services]);
const methodType: const methodType:
| 'unary' | "unary"
| 'server_streaming' | "server_streaming"
| 'client_streaming' | "client_streaming"
| 'streaming' | "streaming"
| 'no-schema' | "no-schema"
| 'no-method' = useMemo(() => { | "no-method" = useMemo(() => {
if (services == null) return 'no-schema'; if (services == null) return "no-schema";
if (activeMethod == null) return 'no-method'; if (activeMethod == null) return "no-method";
if (activeMethod.clientStreaming && activeMethod.serverStreaming) return 'streaming'; if (activeMethod.clientStreaming && activeMethod.serverStreaming) return "streaming";
if (activeMethod.clientStreaming) return 'client_streaming'; if (activeMethod.clientStreaming) return "client_streaming";
if (activeMethod.serverStreaming) return 'server_streaming'; if (activeMethod.serverStreaming) return "server_streaming";
return 'unary'; return "unary";
}, [activeMethod, services]); }, [activeMethod, services]);
if (activeRequest == null) { if (activeRequest == null) {
@@ -106,10 +106,10 @@ export function GrpcConnectionLayout({ style }: Props) {
<div <div
style={style} style={style}
className={classNames( className={classNames(
'x-theme-responsePane', "x-theme-responsePane",
'max-h-full h-full grid grid-rows-[minmax(0,1fr)] grid-cols-1', "max-h-full h-full grid grid-rows-[minmax(0,1fr)] grid-cols-1",
'bg-surface rounded-md border border-border-subtle', "bg-surface rounded-md border border-border-subtle",
'shadow relative', "shadow relative",
)} )}
> >
{grpc.go.error ? ( {grpc.go.error ? (
@@ -119,7 +119,7 @@ export function GrpcConnectionLayout({ style }: Props) {
) : grpcEvents.length >= 0 ? ( ) : grpcEvents.length >= 0 ? (
<GrpcResponsePane activeRequest={activeRequest} methodType={methodType} /> <GrpcResponsePane activeRequest={activeRequest} methodType={methodType} />
) : ( ) : (
<HotkeyList hotkeys={['request.send', 'sidebar.focus', 'url_bar.focus']} /> <HotkeyList hotkeys={["request.send", "sidebar.focus", "url_bar.focus"]} />
)} )}
</div> </div>
) )

View File

@@ -1,27 +1,27 @@
import { linter } from '@codemirror/lint'; import { linter } from "@codemirror/lint";
import type { EditorView } from '@codemirror/view'; import type { EditorView } from "@codemirror/view";
import { jsoncLanguage } from '@shopify/lang-jsonc'; import { jsoncLanguage } from "@shopify/lang-jsonc";
import type { GrpcRequest } from '@yaakapp-internal/models'; import type { GrpcRequest } from "@yaakapp-internal/models";
import { FormattedError, InlineCode, VStack } from '@yaakapp-internal/ui'; import { FormattedError, InlineCode, VStack } from "@yaakapp-internal/ui";
import classNames from 'classnames'; import classNames from "classnames";
import { import {
handleRefresh, handleRefresh,
jsonCompletion, jsonCompletion,
jsonSchemaLinter, jsonSchemaLinter,
stateExtensions, stateExtensions,
updateSchema, updateSchema,
} from 'codemirror-json-schema'; } from "codemirror-json-schema";
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from "react";
import type { ReflectResponseService } from '../hooks/useGrpc'; import type { ReflectResponseService } from "../hooks/useGrpc";
import { showAlert } from '../lib/alert'; import { showAlert } from "../lib/alert";
import { showDialog } from '../lib/dialog'; import { showDialog } from "../lib/dialog";
import { pluralizeCount } from '../lib/pluralize'; import { pluralizeCount } from "../lib/pluralize";
import { Button } from './core/Button'; import { Button } from "./core/Button";
import type { EditorProps } from './core/Editor/Editor'; import type { EditorProps } from "./core/Editor/Editor";
import { Editor } from './core/Editor/LazyEditor'; import { Editor } from "./core/Editor/LazyEditor";
import { GrpcProtoSelectionDialog } from './GrpcProtoSelectionDialog'; import { GrpcProtoSelectionDialog } from "./GrpcProtoSelectionDialog";
type Props = Pick<EditorProps, 'heightMode' | 'onChange' | 'className' | 'forceUpdateKey'> & { type Props = Pick<EditorProps, "heightMode" | "onChange" | "className" | "forceUpdateKey"> & {
services: ReflectResponseService[] | null; services: ReflectResponseService[] | null;
reflectionError?: string; reflectionError?: string;
reflectionLoading?: boolean; reflectionLoading?: boolean;
@@ -55,9 +55,9 @@ export function GrpcEditor({
const s = services.find((s) => s.name === request.service); const s = services.find((s) => s.name === request.service);
if (s == null) { if (s == null) {
console.log('Failed to find service', { service: request.service, services }); console.log("Failed to find service", { service: request.service, services });
showAlert({ showAlert({
id: 'grpc-find-service-error', id: "grpc-find-service-error",
title: "Couldn't Find Service", title: "Couldn't Find Service",
body: ( body: (
<> <>
@@ -70,13 +70,13 @@ export function GrpcEditor({
const schema = s.methods.find((m) => m.name === request.method)?.schema; const schema = s.methods.find((m) => m.name === request.method)?.schema;
if (request.method != null && schema == null) { if (request.method != null && schema == null) {
console.log('Failed to find method', { method: request.method, methods: s?.methods }); console.log("Failed to find method", { method: request.method, methods: s?.methods });
showAlert({ showAlert({
id: 'grpc-find-schema-error', id: "grpc-find-schema-error",
title: "Couldn't Find Method", title: "Couldn't Find Method",
body: ( body: (
<> <>
Failed to find method <InlineCode>{request.method}</InlineCode> for{' '} Failed to find method <InlineCode>{request.method}</InlineCode> for{" "}
<InlineCode>{request.service}</InlineCode> in schema <InlineCode>{request.service}</InlineCode> in schema
</> </>
), ),
@@ -92,12 +92,12 @@ export function GrpcEditor({
updateSchema(editorView, JSON.parse(schema)); updateSchema(editorView, JSON.parse(schema));
} catch (err) { } catch (err) {
showAlert({ showAlert({
id: 'grpc-parse-schema-error', id: "grpc-parse-schema-error",
title: 'Failed to Parse Schema', title: "Failed to Parse Schema",
body: ( body: (
<VStack space={4}> <VStack space={4}>
<p> <p>
For service <InlineCode>{request.service}</InlineCode> and method{' '} For service <InlineCode>{request.service}</InlineCode> and method{" "}
<InlineCode>{request.method}</InlineCode> <InlineCode>{request.method}</InlineCode>
</p> </p>
<FormattedError>{String(err)}</FormattedError> <FormattedError>{String(err)}</FormattedError>
@@ -126,39 +126,39 @@ export function GrpcEditor({
const actions = useMemo( const actions = useMemo(
() => [ () => [
<div key="reflection" className={classNames(services == null && '!opacity-100')}> <div key="reflection" className={classNames(services == null && "!opacity-100")}>
<Button <Button
size="xs" size="xs"
color={ color={
reflectionLoading reflectionLoading
? 'secondary' ? "secondary"
: reflectionUnavailable : reflectionUnavailable
? 'info' ? "info"
: reflectionError : reflectionError
? 'danger' ? "danger"
: 'secondary' : "secondary"
} }
isLoading={reflectionLoading} isLoading={reflectionLoading}
onClick={() => { onClick={() => {
showDialog({ showDialog({
title: 'Configure Schema', title: "Configure Schema",
size: 'md', size: "md",
id: 'reflection-failed', id: "reflection-failed",
render: ({ hide }) => <GrpcProtoSelectionDialog onDone={hide} />, render: ({ hide }) => <GrpcProtoSelectionDialog onDone={hide} />,
}); });
}} }}
> >
{reflectionLoading {reflectionLoading
? 'Inspecting Schema' ? "Inspecting Schema"
: reflectionUnavailable : reflectionUnavailable
? 'Select Proto Files' ? "Select Proto Files"
: reflectionError : reflectionError
? 'Server Error' ? "Server Error"
: protoFiles.length > 0 : protoFiles.length > 0
? pluralizeCount('File', protoFiles.length) ? pluralizeCount("File", protoFiles.length)
: services != null && protoFiles.length === 0 : services != null && protoFiles.length === 0
? 'Schema Detected' ? "Schema Detected"
: 'Select Schema'} : "Select Schema"}
</Button> </Button>
</div>, </div>,
], ],

View File

@@ -1,13 +1,13 @@
import { open } from '@tauri-apps/plugin-dialog'; import { open } from "@tauri-apps/plugin-dialog";
import type { GrpcRequest } from '@yaakapp-internal/models'; import type { GrpcRequest } from "@yaakapp-internal/models";
import { Banner, HStack, Icon, InlineCode, VStack } from '@yaakapp-internal/ui'; import { Banner, HStack, Icon, InlineCode, VStack } from "@yaakapp-internal/ui";
import { useActiveRequest } from '../hooks/useActiveRequest'; import { useActiveRequest } from "../hooks/useActiveRequest";
import { useGrpc } from '../hooks/useGrpc'; import { useGrpc } from "../hooks/useGrpc";
import { useGrpcProtoFiles } from '../hooks/useGrpcProtoFiles'; import { useGrpcProtoFiles } from "../hooks/useGrpcProtoFiles";
import { pluralizeCount } from '../lib/pluralize'; import { pluralizeCount } from "../lib/pluralize";
import { Button } from './core/Button'; import { Button } from "./core/Button";
import { IconButton } from './core/IconButton'; import { IconButton } from "./core/IconButton";
import { Link } from './core/Link'; import { Link } from "./core/Link";
interface Props { interface Props {
onDone: () => void; onDone: () => void;
@@ -15,7 +15,7 @@ interface Props {
export function GrpcProtoSelectionDialog(props: Props) { export function GrpcProtoSelectionDialog(props: Props) {
const request = useActiveRequest(); const request = useActiveRequest();
if (request?.model !== 'grpc_request') return null; if (request?.model !== "grpc_request") return null;
return <GrpcProtoSelectionDialogWithRequest request={request} {...props} />; return <GrpcProtoSelectionDialogWithRequest request={request} {...props} />;
} }
@@ -46,9 +46,9 @@ function GrpcProtoSelectionDialogWithRequest({ request }: Props & { request: Grp
variant="border" variant="border"
onClick={async () => { onClick={async () => {
const selected = await open({ const selected = await open({
title: 'Select Proto Files', title: "Select Proto Files",
multiple: true, multiple: true,
filters: [{ name: 'Proto Files', extensions: ['proto'] }], filters: [{ name: "Proto Files", extensions: ["proto"] }],
}); });
if (selected == null) return; if (selected == null) return;
@@ -64,7 +64,7 @@ function GrpcProtoSelectionDialogWithRequest({ request }: Props & { request: Grp
color="primary" color="primary"
onClick={async () => { onClick={async () => {
const selected = await open({ const selected = await open({
title: 'Select Proto Directory', title: "Select Proto Directory",
directory: true, directory: true,
}); });
if (selected == null) return; if (selected == null) return;
@@ -89,7 +89,7 @@ function GrpcProtoSelectionDialogWithRequest({ request }: Props & { request: Grp
{reflectError && ( {reflectError && (
<Banner color="warning"> <Banner color="warning">
<h1 className="font-bold"> <h1 className="font-bold">
Reflection failed on URL <InlineCode>{request.url || 'n/a'}</InlineCode> Reflection failed on URL <InlineCode>{request.url || "n/a"}</InlineCode>
</h1> </h1>
<p>{reflectError.trim()}</p> <p>{reflectError.trim()}</p>
</Banner> </Banner>
@@ -97,16 +97,16 @@ function GrpcProtoSelectionDialogWithRequest({ request }: Props & { request: Grp
{!serverReflection && services != null && services.length > 0 && ( {!serverReflection && services != null && services.length > 0 && (
<Banner className="flex flex-col gap-2"> <Banner className="flex flex-col gap-2">
<p> <p>
Found services{' '} Found services{" "}
{services?.slice(0, 5).map((s, i) => { {services?.slice(0, 5).map((s, i) => {
return ( return (
<span key={s.name + s.methods.join(',')}> <span key={s.name + s.methods.join(",")}>
<InlineCode>{s.name}</InlineCode> <InlineCode>{s.name}</InlineCode>
{i === services.length - 1 ? '' : i === services.length - 2 ? ' and ' : ', '} {i === services.length - 1 ? "" : i === services.length - 2 ? " and " : ", "}
</span> </span>
); );
})} })}
{services?.length > 5 && pluralizeCount('other', services?.length - 5)} {services?.length > 5 && pluralizeCount("other", services?.length - 5)}
</p> </p>
</Banner> </Banner>
)} )}
@@ -116,13 +116,13 @@ function GrpcProtoSelectionDialogWithRequest({ request }: Props & { request: Grp
Server reflection found services Server reflection found services
{services?.map((s, i) => { {services?.map((s, i) => {
return ( return (
<span key={s.name + s.methods.join(',')}> <span key={s.name + s.methods.join(",")}>
<InlineCode>{s.name}</InlineCode> <InlineCode>{s.name}</InlineCode>
{i === services.length - 1 ? '' : i === services.length - 2 ? ' and ' : ', '} {i === services.length - 1 ? "" : i === services.length - 2 ? " and " : ", "}
</span> </span>
); );
})} })}
. You can override this schema by manually selecting <InlineCode>*.proto</InlineCode>{' '} . You can override this schema by manually selecting <InlineCode>*.proto</InlineCode>{" "}
files. files.
</p> </p>
</Banner> </Banner>
@@ -139,16 +139,16 @@ function GrpcProtoSelectionDialogWithRequest({ request }: Props & { request: Grp
</thead> </thead>
<tbody className="divide-y divide-surface-highlight"> <tbody className="divide-y divide-surface-highlight">
{protoFiles.map((f, i) => { {protoFiles.map((f, i) => {
const parts = f.split('/'); const parts = f.split("/");
return ( return (
// biome-ignore lint/suspicious/noArrayIndexKey: none // biome-ignore lint/suspicious/noArrayIndexKey: none
<tr key={f + i} className="group"> <tr key={f + i} className="group">
<td> <td>
<Icon icon={f.endsWith('.proto') ? 'file_code' : 'folder_code'} /> <Icon icon={f.endsWith(".proto") ? "file_code" : "folder_code"} />
</td> </td>
<td className="pl-1 font-mono text-sm" title={f}> <td className="pl-1 font-mono text-sm" title={f}>
{parts.length > 3 && '.../'} {parts.length > 3 && ".../"}
{parts.slice(-3).join('/')} {parts.slice(-3).join("/")}
</td> </td>
<td className="w-0 py-0.5"> <td className="w-0 py-0.5">
<IconButton <IconButton
@@ -170,10 +170,10 @@ function GrpcProtoSelectionDialogWithRequest({ request }: Props & { request: Grp
)} )}
{reflectionUnimplemented && protoFiles.length === 0 && ( {reflectionUnimplemented && protoFiles.length === 0 && (
<Banner> <Banner>
<InlineCode>{request.url}</InlineCode> doesn&apos;t implement{' '} <InlineCode>{request.url}</InlineCode> doesn&apos;t implement{" "}
<Link href="https://github.com/grpc/grpc/blob/9aa3c5835a4ed6afae9455b63ed45c761d695bca/doc/server-reflection.md"> <Link href="https://github.com/grpc/grpc/blob/9aa3c5835a4ed6afae9455b63ed45c761d695bca/doc/server-reflection.md">
Server Reflection Server Reflection
</Link>{' '} </Link>{" "}
. Please manually add the <InlineCode>.proto</InlineCode> file to get started. . Please manually add the <InlineCode>.proto</InlineCode> file to get started.
</Banner> </Banner>
)} )}

View File

@@ -1,26 +1,26 @@
import { type GrpcRequest, type HttpRequestHeader, patchModel } from '@yaakapp-internal/models'; import { type GrpcRequest, type HttpRequestHeader, patchModel } from "@yaakapp-internal/models";
import { HStack, Icon, useContainerSize, VStack } from '@yaakapp-internal/ui'; import { HStack, Icon, useContainerSize, VStack } from "@yaakapp-internal/ui";
import classNames from 'classnames'; import classNames from "classnames";
import type { CSSProperties } from 'react'; import type { CSSProperties } from "react";
import { useCallback, useMemo, useRef } from 'react'; import { useCallback, useMemo, useRef } from "react";
import { useAuthTab } from '../hooks/useAuthTab'; import { useAuthTab } from "../hooks/useAuthTab";
import type { ReflectResponseService } from '../hooks/useGrpc'; import type { ReflectResponseService } from "../hooks/useGrpc";
import { useHeadersTab } from '../hooks/useHeadersTab'; import { useHeadersTab } from "../hooks/useHeadersTab";
import { useInheritedHeaders } from '../hooks/useInheritedHeaders'; import { useInheritedHeaders } from "../hooks/useInheritedHeaders";
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey'; import { useRequestUpdateKey } from "../hooks/useRequestUpdateKey";
import { resolvedModelName } from '../lib/resolvedModelName'; import { resolvedModelName } from "../lib/resolvedModelName";
import { Button } from './core/Button'; import { Button } from "./core/Button";
import { CountBadge } from './core/CountBadge'; import { CountBadge } from "./core/CountBadge";
import { IconButton } from './core/IconButton'; import { IconButton } from "./core/IconButton";
import { PlainInput } from './core/PlainInput'; import { PlainInput } from "./core/PlainInput";
import { RadioDropdown } from './core/RadioDropdown'; import { RadioDropdown } from "./core/RadioDropdown";
import type { TabItem } from './core/Tabs/Tabs'; import type { TabItem } from "./core/Tabs/Tabs";
import { TabContent, Tabs } from './core/Tabs/Tabs'; import { TabContent, Tabs } from "./core/Tabs/Tabs";
import { GrpcEditor } from './GrpcEditor'; import { GrpcEditor } from "./GrpcEditor";
import { HeadersEditor } from './HeadersEditor'; import { HeadersEditor } from "./HeadersEditor";
import { HttpAuthenticationEditor } from './HttpAuthenticationEditor'; import { HttpAuthenticationEditor } from "./HttpAuthenticationEditor";
import { MarkdownEditor } from './MarkdownEditor'; import { MarkdownEditor } from "./MarkdownEditor";
import { UrlBar } from './UrlBar'; import { UrlBar } from "./UrlBar";
interface Props { interface Props {
style?: CSSProperties; style?: CSSProperties;
@@ -30,12 +30,12 @@ interface Props {
reflectionError?: string; reflectionError?: string;
reflectionLoading?: boolean; reflectionLoading?: boolean;
methodType: methodType:
| 'unary' | "unary"
| 'client_streaming' | "client_streaming"
| 'server_streaming' | "server_streaming"
| 'streaming' | "streaming"
| 'no-schema' | "no-schema"
| 'no-method'; | "no-method";
isStreaming: boolean; isStreaming: boolean;
onCommit: () => void; onCommit: () => void;
onCancel: () => void; onCancel: () => void;
@@ -44,10 +44,10 @@ interface Props {
services: ReflectResponseService[] | null; services: ReflectResponseService[] | null;
} }
const TAB_MESSAGE = 'message'; const TAB_MESSAGE = "message";
const TAB_METADATA = 'metadata'; const TAB_METADATA = "metadata";
const TAB_AUTH = 'auth'; const TAB_AUTH = "auth";
const TAB_DESCRIPTION = 'description'; const TAB_DESCRIPTION = "description";
export function GrpcRequestPane({ export function GrpcRequestPane({
style, style,
@@ -64,7 +64,7 @@ export function GrpcRequestPane({
onSend, onSend,
}: Props) { }: Props) {
const authTab = useAuthTab(TAB_AUTH, activeRequest); const authTab = useAuthTab(TAB_AUTH, activeRequest);
const metadataTab = useHeadersTab(TAB_METADATA, activeRequest, 'Metadata'); const metadataTab = useHeadersTab(TAB_METADATA, activeRequest, "Metadata");
const inheritedHeaders = useInheritedHeaders(activeRequest); const inheritedHeaders = useInheritedHeaders(activeRequest);
const forceUpdateKey = useRequestUpdateKey(activeRequest.id ?? null); const forceUpdateKey = useRequestUpdateKey(activeRequest.id ?? null);
@@ -85,18 +85,18 @@ export function GrpcRequestPane({
const options = const options =
services?.flatMap((s) => services?.flatMap((s) =>
s.methods.map((m) => ({ s.methods.map((m) => ({
label: `${s.name.split('.').pop() ?? s.name}/${m.name}`, label: `${s.name.split(".").pop() ?? s.name}/${m.name}`,
value: `${s.name}/${m.name}`, value: `${s.name}/${m.name}`,
})), })),
) ?? []; ) ?? [];
const value = `${activeRequest?.service ?? ''}/${activeRequest?.method ?? ''}`; const value = `${activeRequest?.service ?? ""}/${activeRequest?.method ?? ""}`;
return { value, options }; return { value, options };
}, [activeRequest?.method, activeRequest?.service, services]); }, [activeRequest?.method, activeRequest?.service, services]);
const handleChangeService = useCallback( const handleChangeService = useCallback(
async (v: string) => { async (v: string) => {
const [serviceName, methodName] = v.split('/', 2); const [serviceName, methodName] = v.split("/", 2);
if (serviceName == null || methodName == null) throw new Error('Should never happen'); if (serviceName == null || methodName == null) throw new Error("Should never happen");
await patchModel(activeRequest, { await patchModel(activeRequest, {
service: serviceName, service: serviceName,
method: methodName, method: methodName,
@@ -110,9 +110,9 @@ export function GrpcRequestPane({
if (activeRequest.service == null || activeRequest.method == null) { if (activeRequest.service == null || activeRequest.method == null) {
alert({ alert({
id: 'grpc-invalid-service-method', id: "grpc-invalid-service-method",
title: 'Error', title: "Error",
body: 'Service or method not selected', body: "Service or method not selected",
}); });
} }
onGo(); onGo();
@@ -125,12 +125,12 @@ export function GrpcRequestPane({
const tabs: TabItem[] = useMemo( const tabs: TabItem[] = useMemo(
() => [ () => [
{ value: TAB_MESSAGE, label: 'Message' }, { value: TAB_MESSAGE, label: "Message" },
...metadataTab, ...metadataTab,
...authTab, ...authTab,
{ {
value: TAB_DESCRIPTION, value: TAB_DESCRIPTION,
label: 'Info', label: "Info",
rightSlot: activeRequest.description && <CountBadge count={true} />, rightSlot: activeRequest.description && <CountBadge count={true} />,
}, },
], ],
@@ -152,14 +152,14 @@ export function GrpcRequestPane({
<div <div
ref={urlContainerEl} ref={urlContainerEl}
className={classNames( className={classNames(
'grid grid-cols-[minmax(0,1fr)_auto] gap-1.5', "grid grid-cols-[minmax(0,1fr)_auto] gap-1.5",
paneWidth === 0 && 'opacity-0', paneWidth === 0 && "opacity-0",
paneWidth > 0 && paneWidth < 400 && '!grid-cols-1', paneWidth > 0 && paneWidth < 400 && "!grid-cols-1",
)} )}
> >
<UrlBar <UrlBar
key={forceUpdateKey} key={forceUpdateKey}
url={activeRequest.url ?? ''} url={activeRequest.url ?? ""}
submitIcon={null} submitIcon={null}
forceUpdateKey={forceUpdateKey} forceUpdateKey={forceUpdateKey}
placeholder="localhost:50051" placeholder="localhost:50051"
@@ -176,13 +176,13 @@ export function GrpcRequestPane({
items={select.options.map((o) => ({ items={select.options.map((o) => ({
label: o.label, label: o.label,
value: o.value, value: o.value,
type: 'default', type: "default",
shortLabel: o.label, shortLabel: o.label,
}))} }))}
itemsAfter={[ itemsAfter={[
{ {
label: 'Refresh', label: "Refresh",
type: 'default', type: "default",
leftSlot: <Icon size="sm" icon="refresh" />, leftSlot: <Icon size="sm" icon="refresh" />,
}, },
]} ]}
@@ -193,14 +193,14 @@ export function GrpcRequestPane({
rightSlot={<Icon size="sm" icon="chevron_down" />} rightSlot={<Icon size="sm" icon="chevron_down" />}
disabled={isStreaming || services == null} disabled={isStreaming || services == null}
className={classNames( className={classNames(
'font-mono text-editor min-w-[5rem] !ring-0', "font-mono text-editor min-w-[5rem] !ring-0",
paneWidth < 400 && 'flex-1', paneWidth < 400 && "flex-1",
)} )}
> >
{select.options.find((o) => o.value === select.value)?.label ?? 'No Schema'} {select.options.find((o) => o.value === select.value)?.label ?? "No Schema"}
</Button> </Button>
</RadioDropdown> </RadioDropdown>
{methodType === 'client_streaming' || methodType === 'streaming' ? ( {methodType === "client_streaming" || methodType === "streaming" ? (
<> <>
{isStreaming && ( {isStreaming && (
<> <>
@@ -223,26 +223,26 @@ export function GrpcRequestPane({
<IconButton <IconButton
size="sm" size="sm"
variant="border" variant="border"
title={isStreaming ? 'Connect' : 'Send'} title={isStreaming ? "Connect" : "Send"}
hotkeyAction="request.send" hotkeyAction="request.send"
onClick={isStreaming ? handleSend : handleConnect} onClick={isStreaming ? handleSend : handleConnect}
icon={isStreaming ? 'send_horizontal' : 'arrow_up_down'} icon={isStreaming ? "send_horizontal" : "arrow_up_down"}
/> />
</> </>
) : ( ) : (
<IconButton <IconButton
size="sm" size="sm"
variant="border" variant="border"
title={methodType === 'unary' ? 'Send' : 'Connect'} title={methodType === "unary" ? "Send" : "Connect"}
hotkeyAction="request.send" hotkeyAction="request.send"
onClick={isStreaming ? onCancel : handleConnect} onClick={isStreaming ? onCancel : handleConnect}
disabled={methodType === 'no-schema' || methodType === 'no-method'} disabled={methodType === "no-schema" || methodType === "no-method"}
icon={ icon={
isStreaming isStreaming
? 'x' ? "x"
: methodType.includes('streaming') : methodType.includes("streaming")
? 'arrow_up_down' ? "arrow_up_down"
: 'send_horizontal' : "send_horizontal"
} }
/> />
)} )}

View File

@@ -1,36 +1,36 @@
import type { GrpcEvent, GrpcRequest } from '@yaakapp-internal/models'; import type { GrpcEvent, GrpcRequest } from "@yaakapp-internal/models";
import { HStack, Icon, type IconProps, LoadingIcon, VStack } from '@yaakapp-internal/ui'; import { HStack, Icon, type IconProps, LoadingIcon, VStack } from "@yaakapp-internal/ui";
import { useAtomValue, useSetAtom } from 'jotai'; import { useAtomValue, useSetAtom } from "jotai";
import type { CSSProperties } from 'react'; import type { CSSProperties } from "react";
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from "react";
import { import {
activeGrpcConnectionAtom, activeGrpcConnectionAtom,
activeGrpcConnections, activeGrpcConnections,
pinnedGrpcConnectionIdAtom, pinnedGrpcConnectionIdAtom,
useGrpcEvents, useGrpcEvents,
} from '../hooks/usePinnedGrpcConnection'; } from "../hooks/usePinnedGrpcConnection";
import { useStateWithDeps } from '../hooks/useStateWithDeps'; import { useStateWithDeps } from "../hooks/useStateWithDeps";
import { Button } from './core/Button'; import { Button } from "./core/Button";
import { Editor } from './core/Editor/LazyEditor'; import { Editor } from "./core/Editor/LazyEditor";
import { EventDetailHeader, EventViewer } from './core/EventViewer'; import { EventDetailHeader, EventViewer } from "./core/EventViewer";
import { EventViewerRow } from './core/EventViewerRow'; import { EventViewerRow } from "./core/EventViewerRow";
import { HotkeyList } from './core/HotkeyList'; import { HotkeyList } from "./core/HotkeyList";
import { KeyValueRow, KeyValueRows } from './core/KeyValueRow'; import { KeyValueRow, KeyValueRows } from "./core/KeyValueRow";
import { EmptyStateText } from './EmptyStateText'; import { EmptyStateText } from "./EmptyStateText";
import { ErrorBoundary } from './ErrorBoundary'; import { ErrorBoundary } from "./ErrorBoundary";
import { RecentGrpcConnectionsDropdown } from './RecentGrpcConnectionsDropdown'; import { RecentGrpcConnectionsDropdown } from "./RecentGrpcConnectionsDropdown";
interface Props { interface Props {
style?: CSSProperties; style?: CSSProperties;
className?: string; className?: string;
activeRequest: GrpcRequest; activeRequest: GrpcRequest;
methodType: methodType:
| 'unary' | "unary"
| 'client_streaming' | "client_streaming"
| 'server_streaming' | "server_streaming"
| 'streaming' | "streaming"
| 'no-schema' | "no-schema"
| 'no-method'; | "no-method";
} }
export function GrpcResponsePane({ style, methodType, activeRequest }: Props) { export function GrpcResponsePane({ style, methodType, activeRequest }: Props) {
@@ -50,10 +50,10 @@ export function GrpcResponsePane({ style, methodType, activeRequest }: Props) {
// Set the active message to the first message received if unary // Set the active message to the first message received if unary
// biome-ignore lint/correctness/useExhaustiveDependencies: none // biome-ignore lint/correctness/useExhaustiveDependencies: none
useEffect(() => { useEffect(() => {
if (events.length === 0 || activeEvent != null || methodType !== 'unary') { if (events.length === 0 || activeEvent != null || methodType !== "unary") {
return; return;
} }
const firstServerMessageIndex = events.findIndex((m) => m.eventType === 'server_message'); const firstServerMessageIndex = events.findIndex((m) => m.eventType === "server_message");
if (firstServerMessageIndex !== -1) { if (firstServerMessageIndex !== -1) {
setActiveEventIndex(firstServerMessageIndex); setActiveEventIndex(firstServerMessageIndex);
} }
@@ -61,7 +61,7 @@ export function GrpcResponsePane({ style, methodType, activeRequest }: Props) {
if (activeConnection == null) { if (activeConnection == null) {
return ( return (
<HotkeyList hotkeys={['request.send', 'model.create', 'sidebar.focus', 'url_bar.focus']} /> <HotkeyList hotkeys={["request.send", "model.create", "sidebar.focus", "url_bar.focus"]} />
); );
} }
@@ -69,7 +69,7 @@ export function GrpcResponsePane({ style, methodType, activeRequest }: Props) {
<HStack className="pl-3 mb-1 font-mono text-sm text-text-subtle overflow-x-auto hide-scrollbars"> <HStack className="pl-3 mb-1 font-mono text-sm text-text-subtle overflow-x-auto hide-scrollbars">
<HStack space={2}> <HStack space={2}>
<span className="whitespace-nowrap">{events.length} Messages</span> <span className="whitespace-nowrap">{events.length} Messages</span>
{activeConnection.state !== 'closed' && ( {activeConnection.state !== "closed" && (
<LoadingIcon size="sm" className="text-text-subtlest" /> <LoadingIcon size="sm" className="text-text-subtlest" />
)} )}
</HStack> </HStack>
@@ -155,8 +155,8 @@ function GrpcEventDetail({
setShowingLarge: (v: boolean) => void; setShowingLarge: (v: boolean) => void;
onClose: () => void; onClose: () => void;
}) { }) {
if (event.eventType === 'client_message' || event.eventType === 'server_message') { if (event.eventType === "client_message" || event.eventType === "server_message") {
const title = `Message ${event.eventType === 'client_message' ? 'Sent' : 'Received'}`; const title = `Message ${event.eventType === "client_message" ? "Sent" : "Received"}`;
return ( return (
<div className="h-full grid grid-rows-[auto_minmax(0,1fr)]"> <div className="h-full grid grid-rows-[auto_minmax(0,1fr)]">
@@ -190,7 +190,7 @@ function GrpcEventDetail({
) : ( ) : (
<Editor <Editor
language="json" language="json"
defaultValue={event.content ?? ''} defaultValue={event.content ?? ""}
wrapLines={false} wrapLines={false}
readOnly={true} readOnly={true}
stateKey={null} stateKey={null}
@@ -212,7 +212,7 @@ function GrpcEventDetail({
<div className="py-2 h-full"> <div className="py-2 h-full">
{Object.keys(event.metadata).length === 0 ? ( {Object.keys(event.metadata).length === 0 ? (
<EmptyStateText> <EmptyStateText>
No {event.eventType === 'connection_end' ? 'trailers' : 'metadata'} No {event.eventType === "connection_end" ? "trailers" : "metadata"}
</EmptyStateText> </EmptyStateText>
) : ( ) : (
<KeyValueRows> <KeyValueRows>
@@ -229,20 +229,20 @@ function GrpcEventDetail({
} }
function getEventDisplay( function getEventDisplay(
eventType: GrpcEvent['eventType'], eventType: GrpcEvent["eventType"],
status: GrpcEvent['status'], status: GrpcEvent["status"],
): { icon: IconProps['icon']; color: IconProps['color']; title: string } { ): { icon: IconProps["icon"]; color: IconProps["color"]; title: string } {
if (eventType === 'server_message') { if (eventType === "server_message") {
return { icon: 'arrow_big_down_dash', color: 'info', title: 'Server message' }; return { icon: "arrow_big_down_dash", color: "info", title: "Server message" };
} }
if (eventType === 'client_message') { if (eventType === "client_message") {
return { icon: 'arrow_big_up_dash', color: 'primary', title: 'Client message' }; return { icon: "arrow_big_up_dash", color: "primary", title: "Client message" };
} }
if (eventType === 'error' || (status != null && status > 0)) { if (eventType === "error" || (status != null && status > 0)) {
return { icon: 'alert_triangle', color: 'danger', title: 'Error' }; return { icon: "alert_triangle", color: "danger", title: "Error" };
} }
if (eventType === 'connection_end') { if (eventType === "connection_end") {
return { icon: 'check', color: 'success', title: 'Connection response' }; return { icon: "check", color: "success", title: "Connection response" };
} }
return { icon: 'info', color: undefined, title: 'Event' }; return { icon: "info", color: undefined, title: "Event" };
} }

View File

@@ -1,19 +1,19 @@
import type { HttpRequestHeader } from '@yaakapp-internal/models'; import type { HttpRequestHeader } from "@yaakapp-internal/models";
import type { GenericCompletionOption } from '@yaakapp-internal/plugins'; import type { GenericCompletionOption } from "@yaakapp-internal/plugins";
import { HStack } from '@yaakapp-internal/ui'; import { HStack } from "@yaakapp-internal/ui";
import { charsets } from '../lib/data/charsets'; import { charsets } from "../lib/data/charsets";
import { connections } from '../lib/data/connections'; import { connections } from "../lib/data/connections";
import { encodings } from '../lib/data/encodings'; import { encodings } from "../lib/data/encodings";
import { headerNames } from '../lib/data/headerNames'; import { headerNames } from "../lib/data/headerNames";
import { mimeTypes } from '../lib/data/mimetypes'; import { mimeTypes } from "../lib/data/mimetypes";
import { CountBadge } from './core/CountBadge'; import { CountBadge } from "./core/CountBadge";
import { DetailsBanner } from './core/DetailsBanner'; import { DetailsBanner } from "./core/DetailsBanner";
import type { GenericCompletionConfig } from './core/Editor/genericCompletion'; import type { GenericCompletionConfig } from "./core/Editor/genericCompletion";
import type { InputProps } from './core/Input'; import type { InputProps } from "./core/Input";
import type { Pair, PairEditorProps } from './core/PairEditor'; import type { Pair, PairEditorProps } from "./core/PairEditor";
import { PairEditorRow } from './core/PairEditor'; import { PairEditorRow } from "./core/PairEditor";
import { ensurePairId } from './core/PairEditor.util'; import { ensurePairId } from "./core/PairEditor.util";
import { PairOrBulkEditor } from './core/PairOrBulkEditor'; import { PairOrBulkEditor } from "./core/PairOrBulkEditor";
type Props = { type Props = {
forceUpdateKey: string; forceUpdateKey: string;
@@ -29,7 +29,7 @@ export function HeadersEditor({
stateKey, stateKey,
headers, headers,
inheritedHeaders, inheritedHeaders,
inheritedHeadersLabel = 'Inherited', inheritedHeadersLabel = "Inherited",
onChange, onChange,
forceUpdateKey, forceUpdateKey,
}: Props) { }: Props) {
@@ -50,8 +50,8 @@ export function HeadersEditor({
<div <div
className={ className={
hasInheritedHeaders hasInheritedHeaders
? '@container w-full h-full grid grid-rows-[auto_minmax(0,1fr)] gap-y-1.5' ? "@container w-full h-full grid grid-rows-[auto_minmax(0,1fr)] gap-y-1.5"
: '@container w-full h-full' : "@container w-full h-full"
} }
> >
{hasInheritedHeaders && ( {hasInheritedHeaders && (
@@ -106,28 +106,28 @@ export function HeadersEditor({
const MIN_MATCH = 3; const MIN_MATCH = 3;
const headerOptionsMap: Record<string, string[]> = { const headerOptionsMap: Record<string, string[]> = {
'content-type': mimeTypes, "content-type": mimeTypes,
accept: ['*/*', ...mimeTypes], accept: ["*/*", ...mimeTypes],
'accept-encoding': encodings, "accept-encoding": encodings,
connection: connections, connection: connections,
'accept-charset': charsets, "accept-charset": charsets,
}; };
const valueType = (pair: Pair): InputProps['type'] => { const valueType = (pair: Pair): InputProps["type"] => {
const name = pair.name.toLowerCase().trim(); const name = pair.name.toLowerCase().trim();
if ( if (
name.includes('authorization') || name.includes("authorization") ||
name.includes('api-key') || name.includes("api-key") ||
name.includes('access-token') || name.includes("access-token") ||
name.includes('auth') || name.includes("auth") ||
name.includes('secret') || name.includes("secret") ||
name.includes('token') || name.includes("token") ||
name === 'cookie' || name === "cookie" ||
name === 'set-cookie' name === "set-cookie"
) { ) {
return 'password'; return "password";
} }
return 'text'; return "text";
}; };
const valueAutocomplete = (headerName: string): GenericCompletionConfig | undefined => { const valueAutocomplete = (headerName: string): GenericCompletionConfig | undefined => {
@@ -135,19 +135,19 @@ const valueAutocomplete = (headerName: string): GenericCompletionConfig | undefi
const options: GenericCompletionOption[] = const options: GenericCompletionOption[] =
headerOptionsMap[name]?.map((o) => ({ headerOptionsMap[name]?.map((o) => ({
label: o, label: o,
type: 'constant', type: "constant",
boost: 1, // Put above other completions boost: 1, // Put above other completions
})) ?? []; })) ?? [];
return { minMatch: MIN_MATCH, options }; return { minMatch: MIN_MATCH, options };
}; };
const nameAutocomplete: PairEditorProps['nameAutocomplete'] = { const nameAutocomplete: PairEditorProps["nameAutocomplete"] = {
minMatch: MIN_MATCH, minMatch: MIN_MATCH,
options: headerNames.map((t) => options: headerNames.map((t) =>
typeof t === 'string' typeof t === "string"
? { ? {
label: t, label: t,
type: 'constant', type: "constant",
boost: 1, // Put above other completions boost: 1, // Put above other completions
} }
: { : {
@@ -158,11 +158,11 @@ const nameAutocomplete: PairEditorProps['nameAutocomplete'] = {
}; };
const validateHttpHeader = (v: string) => { const validateHttpHeader = (v: string) => {
if (v === '') { if (v === "") {
return true; return true;
} }
// Template strings are not allowed so we replace them with a valid example string // Template strings are not allowed so we replace them with a valid example string
const withoutTemplateStrings = v.replace(/\$\{\[\s*[^\]\s]+\s*]}/gi, '123'); const withoutTemplateStrings = v.replace(/\$\{\[\s*[^\]\s]+\s*]}/gi, "123");
return withoutTemplateStrings.match(/^[a-zA-Z0-9-_]+$/) !== null; return withoutTemplateStrings.match(/^[a-zA-Z0-9-_]+$/) !== null;
}; };

View File

@@ -4,23 +4,23 @@ import type {
HttpRequest, HttpRequest,
WebsocketRequest, WebsocketRequest,
Workspace, Workspace,
} from '@yaakapp-internal/models'; } from "@yaakapp-internal/models";
import { patchModel } from '@yaakapp-internal/models'; import { patchModel } from "@yaakapp-internal/models";
import { HStack, Icon, InlineCode } from '@yaakapp-internal/ui'; import { HStack, Icon, InlineCode } from "@yaakapp-internal/ui";
import { useCallback } from 'react'; import { useCallback } from "react";
import { openFolderSettings } from '../commands/openFolderSettings'; import { openFolderSettings } from "../commands/openFolderSettings";
import { openWorkspaceSettings } from '../commands/openWorkspaceSettings'; import { openWorkspaceSettings } from "../commands/openWorkspaceSettings";
import { useHttpAuthenticationConfig } from '../hooks/useHttpAuthenticationConfig'; import { useHttpAuthenticationConfig } from "../hooks/useHttpAuthenticationConfig";
import { useInheritedAuthentication } from '../hooks/useInheritedAuthentication'; import { useInheritedAuthentication } from "../hooks/useInheritedAuthentication";
import { useRenderTemplate } from '../hooks/useRenderTemplate'; import { useRenderTemplate } from "../hooks/useRenderTemplate";
import { resolvedModelName } from '../lib/resolvedModelName'; import { resolvedModelName } from "../lib/resolvedModelName";
import { Dropdown, type DropdownItem } from './core/Dropdown'; import { Dropdown, type DropdownItem } from "./core/Dropdown";
import { IconButton } from './core/IconButton'; import { IconButton } from "./core/IconButton";
import { Input, type InputProps } from './core/Input'; import { Input, type InputProps } from "./core/Input";
import { Link } from './core/Link'; import { Link } from "./core/Link";
import { SegmentedControl } from './core/SegmentedControl'; import { SegmentedControl } from "./core/SegmentedControl";
import { DynamicForm } from './DynamicForm'; import { DynamicForm } from "./DynamicForm";
import { EmptyStateText } from './EmptyStateText'; import { EmptyStateText } from "./EmptyStateText";
interface Props { interface Props {
model: HttpRequest | GrpcRequest | WebsocketRequest | Folder | Workspace; model: HttpRequest | GrpcRequest | WebsocketRequest | Folder | Workspace;
@@ -39,7 +39,7 @@ export function HttpAuthenticationEditor({ model }: Props) {
[model], [model],
); );
if (model.authenticationType === 'none') { if (model.authenticationType === "none") {
return <EmptyStateText>No authentication</EmptyStateText>; return <EmptyStateText>No authentication</EmptyStateText>;
} }
@@ -54,7 +54,7 @@ export function HttpAuthenticationEditor({ model }: Props) {
} }
if (inheritedAuth == null) { if (inheritedAuth == null) {
if (model.model === 'workspace' || model.model === 'folder') { if (model.model === "workspace" || model.model === "folder") {
return ( return (
<EmptyStateText className="flex-col gap-1"> <EmptyStateText className="flex-col gap-1">
<p> <p>
@@ -67,24 +67,24 @@ export function HttpAuthenticationEditor({ model }: Props) {
return <EmptyStateText>No authentication</EmptyStateText>; return <EmptyStateText>No authentication</EmptyStateText>;
} }
if (inheritedAuth.authenticationType === 'none') { if (inheritedAuth.authenticationType === "none") {
return <EmptyStateText>No authentication</EmptyStateText>; return <EmptyStateText>No authentication</EmptyStateText>;
} }
const wasAuthInherited = inheritedAuth?.id !== model.id; const wasAuthInherited = inheritedAuth?.id !== model.id;
if (wasAuthInherited) { if (wasAuthInherited) {
const name = resolvedModelName(inheritedAuth); const name = resolvedModelName(inheritedAuth);
const cta = inheritedAuth.model === 'workspace' ? 'Workspace' : name; const cta = inheritedAuth.model === "workspace" ? "Workspace" : name;
return ( return (
<EmptyStateText> <EmptyStateText>
<p> <p>
Inherited from{' '} Inherited from{" "}
<button <button
type="submit" type="submit"
className="underline hover:text-text" className="underline hover:text-text"
onClick={() => { onClick={() => {
if (inheritedAuth.model === 'folder') openFolderSettings(inheritedAuth.id, 'auth'); if (inheritedAuth.model === "folder") openFolderSettings(inheritedAuth.id, "auth");
else openWorkspaceSettings('auth'); else openWorkspaceSettings("auth");
}} }}
> >
{cta} {cta}
@@ -104,24 +104,24 @@ export function HttpAuthenticationEditor({ model }: Props) {
name="enabled" name="enabled"
value={ value={
model.authentication.disabled === false || model.authentication.disabled == null model.authentication.disabled === false || model.authentication.disabled == null
? '__TRUE__' ? "__TRUE__"
: model.authentication.disabled === true : model.authentication.disabled === true
? '__FALSE__' ? "__FALSE__"
: '__DYNAMIC__' : "__DYNAMIC__"
} }
options={[ options={[
{ label: 'Enabled', value: '__TRUE__' }, { label: "Enabled", value: "__TRUE__" },
{ label: 'Disabled', value: '__FALSE__' }, { label: "Disabled", value: "__FALSE__" },
{ label: 'Enabled when...', value: '__DYNAMIC__' }, { label: "Enabled when...", value: "__DYNAMIC__" },
]} ]}
onChange={async (enabled) => { onChange={async (enabled) => {
let disabled: boolean | string; let disabled: boolean | string;
if (enabled === '__TRUE__') { if (enabled === "__TRUE__") {
disabled = false; disabled = false;
} else if (enabled === '__FALSE__') { } else if (enabled === "__FALSE__") {
disabled = true; disabled = true;
} else { } else {
disabled = ''; disabled = "";
} }
await handleChange({ ...model.authentication, disabled }); await handleChange({ ...model.authentication, disabled });
}} }}
@@ -145,7 +145,7 @@ export function HttpAuthenticationEditor({ model }: Props) {
</Dropdown> </Dropdown>
)} )}
</HStack> </HStack>
{typeof model.authentication.disabled === 'string' && ( {typeof model.authentication.disabled === "string" && (
<div className="mt-3"> <div className="mt-3">
<AuthenticationDisabledInput <AuthenticationDisabledInput
className="w-full" className="w-full"
@@ -176,14 +176,14 @@ function AuthenticationDisabledInput({
className, className,
}: { }: {
value: string; value: string;
onChange: InputProps['onChange']; onChange: InputProps["onChange"];
stateKey: string; stateKey: string;
className?: string; className?: string;
}) { }) {
const rendered = useRenderTemplate({ const rendered = useRenderTemplate({
template: value, template: value,
enabled: true, enabled: true,
purpose: 'preview', purpose: "preview",
refreshKey: value, refreshKey: value,
}); });
@@ -198,7 +198,7 @@ function AuthenticationDisabledInput({
rightSlot={ rightSlot={
<div className="px-1 flex items-center"> <div className="px-1 flex items-center">
<div className="rounded-full bg-surface-highlight text-xs px-1.5 py-0.5 text-text-subtle whitespace-nowrap"> <div className="rounded-full bg-surface-highlight text-xs px-1.5 py-0.5 text-text-subtle whitespace-nowrap">
{rendered.isPending ? 'loading' : rendered.data ? 'enabled' : 'disabled'} {rendered.isPending ? "loading" : rendered.data ? "enabled" : "disabled"}
</div> </div>
</div> </div>
} }

View File

@@ -1,16 +1,16 @@
import type { HttpRequest } from '@yaakapp-internal/models'; import type { HttpRequest } from "@yaakapp-internal/models";
import type { SlotProps } from '@yaakapp-internal/ui'; import type { SlotProps } from "@yaakapp-internal/ui";
import { SplitLayout } from '@yaakapp-internal/ui'; import { SplitLayout } from "@yaakapp-internal/ui";
import classNames from 'classnames'; import classNames from "classnames";
import { useAtomValue } from 'jotai'; import { useAtomValue } from "jotai";
import type { CSSProperties } from 'react'; import type { CSSProperties } from "react";
import { useCurrentGraphQLSchema } from '../hooks/useIntrospectGraphQL'; import { useCurrentGraphQLSchema } from "../hooks/useIntrospectGraphQL";
import { activeWorkspaceAtom } from '../hooks/useActiveWorkspace'; import { activeWorkspaceAtom } from "../hooks/useActiveWorkspace";
import { workspaceLayoutAtom } from '../lib/atoms'; import { workspaceLayoutAtom } from "../lib/atoms";
import { GraphQLDocsExplorer } from './graphql/GraphQLDocsExplorer'; import { GraphQLDocsExplorer } from "./graphql/GraphQLDocsExplorer";
import { showGraphQLDocExplorerAtom } from './graphql/graphqlAtoms'; import { showGraphQLDocExplorerAtom } from "./graphql/graphqlAtoms";
import { HttpRequestPane } from './HttpRequestPane'; import { HttpRequestPane } from "./HttpRequestPane";
import { HttpResponsePane } from './HttpResponsePane'; import { HttpResponsePane } from "./HttpResponsePane";
interface Props { interface Props {
activeRequest: HttpRequest; activeRequest: HttpRequest;
@@ -22,9 +22,9 @@ export function HttpRequestLayout({ activeRequest, style }: Props) {
const graphQLSchema = useCurrentGraphQLSchema(activeRequest); const graphQLSchema = useCurrentGraphQLSchema(activeRequest);
const workspaceLayout = useAtomValue(workspaceLayoutAtom); const workspaceLayout = useAtomValue(workspaceLayoutAtom);
const activeWorkspace = useAtomValue(activeWorkspaceAtom); const activeWorkspace = useAtomValue(activeWorkspaceAtom);
const wsId = activeWorkspace?.id ?? 'n/a'; const wsId = activeWorkspace?.id ?? "n/a";
const requestResponseSplit = ({ style }: Pick<SlotProps, 'style'>) => ( const requestResponseSplit = ({ style }: Pick<SlotProps, "style">) => (
<SplitLayout <SplitLayout
storageKey={`http_layout::${wsId}`} storageKey={`http_layout::${wsId}`}
className="p-3 gap-1.5" className="p-3 gap-1.5"
@@ -34,7 +34,7 @@ export function HttpRequestLayout({ activeRequest, style }: Props) {
<HttpRequestPane <HttpRequestPane
style={style} style={style}
activeRequest={activeRequest} activeRequest={activeRequest}
fullHeight={orientation === 'horizontal'} fullHeight={orientation === "horizontal"}
/> />
)} )}
secondSlot={({ style }) => ( secondSlot={({ style }) => (
@@ -44,7 +44,7 @@ export function HttpRequestLayout({ activeRequest, style }: Props) {
); );
if ( if (
activeRequest.bodyType === 'graphql' && activeRequest.bodyType === "graphql" &&
showGraphQLDocExplorer[activeRequest.id] !== undefined && showGraphQLDocExplorer[activeRequest.id] !== undefined &&
graphQLSchema != null graphQLSchema != null
) { ) {
@@ -57,7 +57,7 @@ export function HttpRequestLayout({ activeRequest, style }: Props) {
<GraphQLDocsExplorer <GraphQLDocsExplorer
requestId={activeRequest.id} requestId={activeRequest.id}
schema={graphQLSchema} schema={graphQLSchema}
className={classNames(orientation === 'horizontal' && '!ml-0')} className={classNames(orientation === "horizontal" && "!ml-0")}
style={style} style={style}
/> />
)} )}

View File

@@ -1,24 +1,24 @@
import type { HttpRequest } from '@yaakapp-internal/models'; import type { HttpRequest } from "@yaakapp-internal/models";
import { patchModel } from '@yaakapp-internal/models'; import { patchModel } from "@yaakapp-internal/models";
import type { GenericCompletionOption } from '@yaakapp-internal/plugins'; import type { GenericCompletionOption } from "@yaakapp-internal/plugins";
import classNames from 'classnames'; import classNames from "classnames";
import { atom, useAtomValue } from 'jotai'; import { atom, useAtomValue } from "jotai";
import type { CSSProperties } from 'react'; import type { CSSProperties } from "react";
import { lazy, Suspense, useCallback, useMemo, useRef, useState } from 'react'; import { lazy, Suspense, useCallback, useMemo, useRef, useState } from "react";
import { activeRequestIdAtom } from '../hooks/useActiveRequestId'; import { activeRequestIdAtom } from "../hooks/useActiveRequestId";
import { allRequestsAtom } from '../hooks/useAllRequests'; import { allRequestsAtom } from "../hooks/useAllRequests";
import { useAuthTab } from '../hooks/useAuthTab'; import { useAuthTab } from "../hooks/useAuthTab";
import { useCancelHttpResponse } from '../hooks/useCancelHttpResponse'; import { useCancelHttpResponse } from "../hooks/useCancelHttpResponse";
import { useHeadersTab } from '../hooks/useHeadersTab'; import { useHeadersTab } from "../hooks/useHeadersTab";
import { useImportCurl } from '../hooks/useImportCurl'; import { useImportCurl } from "../hooks/useImportCurl";
import { useInheritedHeaders } from '../hooks/useInheritedHeaders'; import { useInheritedHeaders } from "../hooks/useInheritedHeaders";
import { usePinnedHttpResponse } from '../hooks/usePinnedHttpResponse'; import { usePinnedHttpResponse } from "../hooks/usePinnedHttpResponse";
import { useRequestEditor, useRequestEditorEvent } from '../hooks/useRequestEditor'; import { useRequestEditor, useRequestEditorEvent } from "../hooks/useRequestEditor";
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey'; import { useRequestUpdateKey } from "../hooks/useRequestUpdateKey";
import { useSendAnyHttpRequest } from '../hooks/useSendAnyHttpRequest'; import { useSendAnyHttpRequest } from "../hooks/useSendAnyHttpRequest";
import { deepEqualAtom } from '../lib/atoms'; import { deepEqualAtom } from "../lib/atoms";
import { languageFromContentType } from '../lib/contentType'; import { languageFromContentType } from "../lib/contentType";
import { generateId } from '../lib/generateId'; import { generateId } from "../lib/generateId";
import { import {
BODY_TYPE_BINARY, BODY_TYPE_BINARY,
BODY_TYPE_FORM_MULTIPART, BODY_TYPE_FORM_MULTIPART,
@@ -29,33 +29,33 @@ import {
BODY_TYPE_OTHER, BODY_TYPE_OTHER,
BODY_TYPE_XML, BODY_TYPE_XML,
getContentTypeFromHeaders, getContentTypeFromHeaders,
} from '../lib/model_util'; } from "../lib/model_util";
import { prepareImportQuerystring } from '../lib/prepareImportQuerystring'; import { prepareImportQuerystring } from "../lib/prepareImportQuerystring";
import { resolvedModelName } from '../lib/resolvedModelName'; import { resolvedModelName } from "../lib/resolvedModelName";
import { showToast } from '../lib/toast'; import { showToast } from "../lib/toast";
import { BinaryFileEditor } from './BinaryFileEditor'; import { BinaryFileEditor } from "./BinaryFileEditor";
import { ConfirmLargeRequestBody } from './ConfirmLargeRequestBody'; import { ConfirmLargeRequestBody } from "./ConfirmLargeRequestBody";
import { CountBadge } from './core/CountBadge'; import { CountBadge } from "./core/CountBadge";
import type { GenericCompletionConfig } from './core/Editor/genericCompletion'; import type { GenericCompletionConfig } from "./core/Editor/genericCompletion";
import { Editor } from './core/Editor/LazyEditor'; import { Editor } from "./core/Editor/LazyEditor";
import { InlineCode } from '@yaakapp-internal/ui'; import { InlineCode } from "@yaakapp-internal/ui";
import type { Pair } from './core/PairEditor'; import type { Pair } from "./core/PairEditor";
import { PlainInput } from './core/PlainInput'; import { PlainInput } from "./core/PlainInput";
import type { TabItem, TabsRef } from './core/Tabs/Tabs'; import type { TabItem, TabsRef } from "./core/Tabs/Tabs";
import { setActiveTab, TabContent, Tabs } from './core/Tabs/Tabs'; import { setActiveTab, TabContent, Tabs } from "./core/Tabs/Tabs";
import { EmptyStateText } from './EmptyStateText'; import { EmptyStateText } from "./EmptyStateText";
import { FormMultipartEditor } from './FormMultipartEditor'; import { FormMultipartEditor } from "./FormMultipartEditor";
import { FormUrlencodedEditor } from './FormUrlencodedEditor'; import { FormUrlencodedEditor } from "./FormUrlencodedEditor";
import { HeadersEditor } from './HeadersEditor'; import { HeadersEditor } from "./HeadersEditor";
import { HttpAuthenticationEditor } from './HttpAuthenticationEditor'; import { HttpAuthenticationEditor } from "./HttpAuthenticationEditor";
import { JsonBodyEditor } from './JsonBodyEditor'; import { JsonBodyEditor } from "./JsonBodyEditor";
import { MarkdownEditor } from './MarkdownEditor'; import { MarkdownEditor } from "./MarkdownEditor";
import { RequestMethodDropdown } from './RequestMethodDropdown'; import { RequestMethodDropdown } from "./RequestMethodDropdown";
import { UrlBar } from './UrlBar'; import { UrlBar } from "./UrlBar";
import { UrlParametersEditor } from './UrlParameterEditor'; import { UrlParametersEditor } from "./UrlParameterEditor";
const GraphQLEditor = lazy(() => const GraphQLEditor = lazy(() =>
import('./graphql/GraphQLEditor').then((m) => ({ default: m.GraphQLEditor })), import("./graphql/GraphQLEditor").then((m) => ({ default: m.GraphQLEditor })),
); );
interface Props { interface Props {
@@ -65,19 +65,19 @@ interface Props {
activeRequest: HttpRequest; activeRequest: HttpRequest;
} }
const TAB_BODY = 'body'; const TAB_BODY = "body";
const TAB_PARAMS = 'params'; const TAB_PARAMS = "params";
const TAB_HEADERS = 'headers'; const TAB_HEADERS = "headers";
const TAB_AUTH = 'auth'; const TAB_AUTH = "auth";
const TAB_DESCRIPTION = 'description'; const TAB_DESCRIPTION = "description";
const TABS_STORAGE_KEY = 'http_request_tabs'; const TABS_STORAGE_KEY = "http_request_tabs";
const nonActiveRequestUrlsAtom = atom((get) => { const nonActiveRequestUrlsAtom = atom((get) => {
const activeRequestId = get(activeRequestIdAtom); const activeRequestId = get(activeRequestIdAtom);
const requests = get(allRequestsAtom); const requests = get(allRequestsAtom);
return requests return requests
.filter((r) => r.id !== activeRequestId) .filter((r) => r.id !== activeRequestId)
.map((r): GenericCompletionOption => ({ type: 'constant', label: r.url })); .map((r): GenericCompletionOption => ({ type: "constant", label: r.url }));
}); });
const memoNotActiveRequestUrlsAtom = deepEqualAtom(nonActiveRequestUrlsAtom); const memoNotActiveRequestUrlsAtom = deepEqualAtom(nonActiveRequestUrlsAtom);
@@ -94,22 +94,26 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
const inheritedHeaders = useInheritedHeaders(activeRequest); const inheritedHeaders = useInheritedHeaders(activeRequest);
// Listen for event to focus the params tab (e.g., when clicking a :param in the URL) // Listen for event to focus the params tab (e.g., when clicking a :param in the URL)
useRequestEditorEvent('request_pane.focus_tab', () => { useRequestEditorEvent(
tabsRef.current?.setActiveTab(TAB_PARAMS); "request_pane.focus_tab",
}, []); () => {
tabsRef.current?.setActiveTab(TAB_PARAMS);
},
[],
);
const handleContentTypeChange = useCallback( const handleContentTypeChange = useCallback(
async (contentType: string | null, patch: Partial<Omit<HttpRequest, 'headers'>> = {}) => { async (contentType: string | null, patch: Partial<Omit<HttpRequest, "headers">> = {}) => {
if (activeRequest == null) { if (activeRequest == null) {
console.error('Failed to get active request to update', activeRequest); console.error("Failed to get active request to update", activeRequest);
return; return;
} }
const headers = activeRequest.headers.filter((h) => h.name.toLowerCase() !== 'content-type'); const headers = activeRequest.headers.filter((h) => h.name.toLowerCase() !== "content-type");
if (contentType != null) { if (contentType != null) {
headers.push({ headers.push({
name: 'Content-Type', name: "Content-Type",
value: contentType, value: contentType,
enabled: true, enabled: true,
id: generateId(), id: generateId(),
@@ -125,7 +129,7 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
const { urlParameterPairs, urlParametersKey } = useMemo(() => { const { urlParameterPairs, urlParametersKey } = useMemo(() => {
const placeholderNames = Array.from(activeRequest.url.matchAll(/\/(:[^/]+)/g)).map( const placeholderNames = Array.from(activeRequest.url.matchAll(/\/(:[^/]+)/g)).map(
(m) => m[1] ?? '', (m) => m[1] ?? "",
); );
const nonEmptyParameters = activeRequest.urlParameters.filter((p) => p.name || p.value); const nonEmptyParameters = activeRequest.urlParameters.filter((p) => p.name || p.value);
const items: Pair[] = [...nonEmptyParameters]; const items: Pair[] = [...nonEmptyParameters];
@@ -134,10 +138,10 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
if (item) { if (item) {
item.readOnlyName = true; item.readOnlyName = true;
} else { } else {
items.push({ name, value: '', enabled: true, readOnlyName: true, id: generateId() }); items.push({ name, value: "", enabled: true, readOnlyName: true, id: generateId() });
} }
} }
return { urlParameterPairs: items, urlParametersKey: placeholderNames.join(',') }; return { urlParameterPairs: items, urlParametersKey: placeholderNames.join(",") };
}, [activeRequest.url, activeRequest.urlParameters]); }, [activeRequest.url, activeRequest.urlParameters]);
let numParams = 0; let numParams = 0;
@@ -158,21 +162,21 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
options: { options: {
value: activeRequest.bodyType, value: activeRequest.bodyType,
items: [ items: [
{ type: 'separator', label: 'Form Data' }, { type: "separator", label: "Form Data" },
{ label: 'Url Encoded', value: BODY_TYPE_FORM_URLENCODED }, { label: "Url Encoded", value: BODY_TYPE_FORM_URLENCODED },
{ label: 'Multi-Part', value: BODY_TYPE_FORM_MULTIPART }, { label: "Multi-Part", value: BODY_TYPE_FORM_MULTIPART },
{ type: 'separator', label: 'Text Content' }, { type: "separator", label: "Text Content" },
{ label: 'GraphQL', value: BODY_TYPE_GRAPHQL }, { label: "GraphQL", value: BODY_TYPE_GRAPHQL },
{ label: 'JSON', value: BODY_TYPE_JSON }, { label: "JSON", value: BODY_TYPE_JSON },
{ label: 'XML', value: BODY_TYPE_XML }, { label: "XML", value: BODY_TYPE_XML },
{ {
label: 'Other', label: "Other",
value: BODY_TYPE_OTHER, value: BODY_TYPE_OTHER,
shortLabel: nameOfContentTypeOr(contentType, 'Other'), shortLabel: nameOfContentTypeOr(contentType, "Other"),
}, },
{ type: 'separator', label: 'Other' }, { type: "separator", label: "Other" },
{ label: 'Binary File', value: BODY_TYPE_BINARY }, { label: "Binary File", value: BODY_TYPE_BINARY },
{ label: 'No Body', shortLabel: 'Body', value: BODY_TYPE_NONE }, { label: "No Body", shortLabel: "Body", value: BODY_TYPE_NONE },
], ],
onChange: async (bodyType) => { onChange: async (bodyType) => {
if (bodyType === activeRequest.bodyType) return; if (bodyType === activeRequest.bodyType) return;
@@ -180,7 +184,7 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
const showMethodToast = (newMethod: string) => { const showMethodToast = (newMethod: string) => {
if (activeRequest.method.toLowerCase() === newMethod.toLowerCase()) return; if (activeRequest.method.toLowerCase() === newMethod.toLowerCase()) return;
showToast({ showToast({
id: 'switched-method', id: "switched-method",
message: ( message: (
<> <>
Request method switched to <InlineCode>POST</InlineCode> Request method switched to <InlineCode>POST</InlineCode>
@@ -202,16 +206,16 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
) { ) {
const isDefaultishRequest = const isDefaultishRequest =
activeRequest.bodyType === BODY_TYPE_NONE && activeRequest.bodyType === BODY_TYPE_NONE &&
activeRequest.method.toLowerCase() === 'get'; activeRequest.method.toLowerCase() === "get";
const requiresPost = bodyType === BODY_TYPE_FORM_MULTIPART; const requiresPost = bodyType === BODY_TYPE_FORM_MULTIPART;
if (isDefaultishRequest || requiresPost) { if (isDefaultishRequest || requiresPost) {
patch.method = 'POST'; patch.method = "POST";
showMethodToast(patch.method); showMethodToast(patch.method);
} }
newContentType = bodyType === BODY_TYPE_OTHER ? 'text/plain' : bodyType; newContentType = bodyType === BODY_TYPE_OTHER ? "text/plain" : bodyType;
} else if (bodyType === BODY_TYPE_GRAPHQL) { } else if (bodyType === BODY_TYPE_GRAPHQL) {
patch.method = 'POST'; patch.method = "POST";
newContentType = 'application/json'; newContentType = "application/json";
showMethodToast(patch.method); showMethodToast(patch.method);
} }
@@ -226,13 +230,13 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
{ {
value: TAB_PARAMS, value: TAB_PARAMS,
rightSlot: <CountBadge count={urlParameterPairs.length} />, rightSlot: <CountBadge count={urlParameterPairs.length} />,
label: 'Params', label: "Params",
}, },
...headersTab, ...headersTab,
...authTab, ...authTab,
{ {
value: TAB_DESCRIPTION, value: TAB_DESCRIPTION,
label: 'Info', label: "Info",
}, },
], ],
[ [
@@ -253,7 +257,7 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
const { mutate: importCurl } = useImportCurl(); const { mutate: importCurl } = useImportCurl();
const handleBodyChange = useCallback( const handleBodyChange = useCallback(
(body: HttpRequest['body']) => patchModel(activeRequest, { body }), (body: HttpRequest["body"]) => patchModel(activeRequest, { body }),
[activeRequest], [activeRequest],
); );
@@ -271,8 +275,8 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
autocompleteUrls.length > 0 autocompleteUrls.length > 0
? autocompleteUrls ? autocompleteUrls
: [ : [
{ label: 'http://', type: 'constant' }, { label: "http://", type: "constant" },
{ label: 'https://', type: 'constant' }, { label: "https://", type: "constant" },
], ],
}), }),
[autocompleteUrls], [autocompleteUrls],
@@ -280,7 +284,7 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
const handlePaste = useCallback( const handlePaste = useCallback(
async (e: ClipboardEvent, text: string) => { async (e: ClipboardEvent, text: string) => {
if (text.startsWith('curl ')) { if (text.startsWith("curl ")) {
importCurl({ overwriteRequestId: activeRequestId, command: text }); importCurl({ overwriteRequestId: activeRequestId, command: text });
} else { } else {
const patch = prepareImportQuerystring(text); const patch = prepareImportQuerystring(text);
@@ -318,7 +322,7 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
return ( return (
<div <div
style={style} style={style}
className={classNames(className, 'h-full grid grid-rows-[auto_minmax(0,1fr)] grid-cols-1')} className={classNames(className, "h-full grid grid-rows-[auto_minmax(0,1fr)] grid-cols-1")}
> >
{activeRequest && ( {activeRequest && (
<> <>
@@ -338,7 +342,7 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
</div> </div>
} }
forceUpdateKey={updateKey} forceUpdateKey={updateKey}
isLoading={activeResponse != null && activeResponse.state !== 'closed'} isLoading={activeResponse != null && activeResponse.state !== "closed"}
/> />
<Tabs <Tabs
ref={tabsRef} ref={tabsRef}
@@ -373,7 +377,7 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
{activeRequest.bodyType === BODY_TYPE_JSON ? ( {activeRequest.bodyType === BODY_TYPE_JSON ? (
<JsonBodyEditor <JsonBodyEditor
forceUpdateKey={forceUpdateKey} forceUpdateKey={forceUpdateKey}
heightMode={fullHeight ? 'full' : 'auto'} heightMode={fullHeight ? "full" : "auto"}
request={activeRequest} request={activeRequest}
/> />
) : activeRequest.bodyType === BODY_TYPE_XML ? ( ) : activeRequest.bodyType === BODY_TYPE_XML ? (
@@ -382,8 +386,8 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
autocompleteFunctions autocompleteFunctions
autocompleteVariables autocompleteVariables
placeholder="..." placeholder="..."
heightMode={fullHeight ? 'full' : 'auto'} heightMode={fullHeight ? "full" : "auto"}
defaultValue={`${activeRequest.body?.text ?? ''}`} defaultValue={`${activeRequest.body?.text ?? ""}`}
language="xml" language="xml"
onChange={handleBodyTextChange} onChange={handleBodyTextChange}
stateKey={`xml.${activeRequest.id}`} stateKey={`xml.${activeRequest.id}`}
@@ -417,15 +421,15 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
onChange={(body) => patchModel(activeRequest, { body })} onChange={(body) => patchModel(activeRequest, { body })}
onChangeContentType={handleContentTypeChange} onChangeContentType={handleContentTypeChange}
/> />
) : typeof activeRequest.bodyType === 'string' ? ( ) : typeof activeRequest.bodyType === "string" ? (
<Editor <Editor
forceUpdateKey={forceUpdateKey} forceUpdateKey={forceUpdateKey}
autocompleteFunctions autocompleteFunctions
autocompleteVariables autocompleteVariables
language={languageFromContentType(contentType)} language={languageFromContentType(contentType)}
placeholder="..." placeholder="..."
heightMode={fullHeight ? 'full' : 'auto'} heightMode={fullHeight ? "full" : "auto"}
defaultValue={`${activeRequest.body?.text ?? ''}`} defaultValue={`${activeRequest.body?.text ?? ""}`}
onChange={handleBodyTextChange} onChange={handleBodyTextChange}
stateKey={`other.${activeRequest.id}`} stateKey={`other.${activeRequest.id}`}
/> />
@@ -465,8 +469,8 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
function nameOfContentTypeOr(contentType: string | null, fallback: string) { function nameOfContentTypeOr(contentType: string | null, fallback: string) {
const language = languageFromContentType(contentType); const language = languageFromContentType(contentType);
if (language === 'markdown') { if (language === "markdown") {
return 'Markdown'; return "Markdown";
} }
return fallback; return fallback;
} }

View File

@@ -1,46 +1,46 @@
import type { HttpResponse, HttpResponseEvent } from '@yaakapp-internal/models'; import type { HttpResponse, HttpResponseEvent } from "@yaakapp-internal/models";
import { Banner, HStack, Icon, LoadingIcon, VStack } from '@yaakapp-internal/ui'; import { Banner, HStack, Icon, LoadingIcon, VStack } from "@yaakapp-internal/ui";
import classNames from 'classnames'; import classNames from "classnames";
import type { ComponentType, CSSProperties } from 'react'; import type { ComponentType, CSSProperties } from "react";
import { lazy, Suspense, useMemo } from 'react'; import { lazy, Suspense, useMemo } from "react";
import { useCancelHttpResponse } from '../hooks/useCancelHttpResponse'; import { useCancelHttpResponse } from "../hooks/useCancelHttpResponse";
import { useHttpResponseEvents } from '../hooks/useHttpResponseEvents'; import { useHttpResponseEvents } from "../hooks/useHttpResponseEvents";
import { usePinnedHttpResponse } from '../hooks/usePinnedHttpResponse'; import { usePinnedHttpResponse } from "../hooks/usePinnedHttpResponse";
import { useResponseBodyBytes, useResponseBodyText } from '../hooks/useResponseBodyText'; import { useResponseBodyBytes, useResponseBodyText } from "../hooks/useResponseBodyText";
import { useResponseViewMode } from '../hooks/useResponseViewMode'; import { useResponseViewMode } from "../hooks/useResponseViewMode";
import { useTimelineViewMode } from '../hooks/useTimelineViewMode'; import { useTimelineViewMode } from "../hooks/useTimelineViewMode";
import { getMimeTypeFromContentType } from '../lib/contentType'; import { getMimeTypeFromContentType } from "../lib/contentType";
import { getContentTypeFromHeaders, getCookieCounts } from '../lib/model_util'; import { getContentTypeFromHeaders, getCookieCounts } from "../lib/model_util";
import { ConfirmLargeResponse } from './ConfirmLargeResponse'; import { ConfirmLargeResponse } from "./ConfirmLargeResponse";
import { ConfirmLargeResponseRequest } from './ConfirmLargeResponseRequest'; import { ConfirmLargeResponseRequest } from "./ConfirmLargeResponseRequest";
import { Button } from './core/Button'; import { Button } from "./core/Button";
import { CountBadge } from './core/CountBadge'; import { CountBadge } from "./core/CountBadge";
import { HotkeyList } from './core/HotkeyList'; import { HotkeyList } from "./core/HotkeyList";
import { HttpResponseDurationTag } from './core/HttpResponseDurationTag'; import { HttpResponseDurationTag } from "./core/HttpResponseDurationTag";
import { HttpStatusTag } from './core/HttpStatusTag'; import { HttpStatusTag } from "./core/HttpStatusTag";
import { PillButton } from './core/PillButton'; import { PillButton } from "./core/PillButton";
import { SizeTag } from './core/SizeTag'; import { SizeTag } from "./core/SizeTag";
import type { TabItem } from './core/Tabs/Tabs'; import type { TabItem } from "./core/Tabs/Tabs";
import { TabContent, Tabs } from './core/Tabs/Tabs'; import { TabContent, Tabs } from "./core/Tabs/Tabs";
import { Tooltip } from './core/Tooltip'; import { Tooltip } from "./core/Tooltip";
import { EmptyStateText } from './EmptyStateText'; import { EmptyStateText } from "./EmptyStateText";
import { ErrorBoundary } from './ErrorBoundary'; import { ErrorBoundary } from "./ErrorBoundary";
import { HttpResponseTimeline } from './HttpResponseTimeline'; import { HttpResponseTimeline } from "./HttpResponseTimeline";
import { RecentHttpResponsesDropdown } from './RecentHttpResponsesDropdown'; import { RecentHttpResponsesDropdown } from "./RecentHttpResponsesDropdown";
import { RequestBodyViewer } from './RequestBodyViewer'; import { RequestBodyViewer } from "./RequestBodyViewer";
import { ResponseCookies } from './ResponseCookies'; import { ResponseCookies } from "./ResponseCookies";
import { ResponseHeaders } from './ResponseHeaders'; import { ResponseHeaders } from "./ResponseHeaders";
import { AudioViewer } from './responseViewers/AudioViewer'; import { AudioViewer } from "./responseViewers/AudioViewer";
import { CsvViewer } from './responseViewers/CsvViewer'; import { CsvViewer } from "./responseViewers/CsvViewer";
import { EventStreamViewer } from './responseViewers/EventStreamViewer'; import { EventStreamViewer } from "./responseViewers/EventStreamViewer";
import { HTMLOrTextViewer } from './responseViewers/HTMLOrTextViewer'; import { HTMLOrTextViewer } from "./responseViewers/HTMLOrTextViewer";
import { ImageViewer } from './responseViewers/ImageViewer'; import { ImageViewer } from "./responseViewers/ImageViewer";
import { MultipartViewer } from './responseViewers/MultipartViewer'; import { MultipartViewer } from "./responseViewers/MultipartViewer";
import { SvgViewer } from './responseViewers/SvgViewer'; import { SvgViewer } from "./responseViewers/SvgViewer";
import { VideoViewer } from './responseViewers/VideoViewer'; import { VideoViewer } from "./responseViewers/VideoViewer";
const PdfViewer = lazy(() => const PdfViewer = lazy(() =>
import('./responseViewers/PdfViewer').then((m) => ({ default: m.PdfViewer })), import("./responseViewers/PdfViewer").then((m) => ({ default: m.PdfViewer })),
); );
interface Props { interface Props {
@@ -49,13 +49,13 @@ interface Props {
activeRequestId: string; activeRequestId: string;
} }
const TAB_BODY = 'body'; const TAB_BODY = "body";
const TAB_REQUEST = 'request'; const TAB_REQUEST = "request";
const TAB_HEADERS = 'headers'; const TAB_HEADERS = "headers";
const TAB_COOKIES = 'cookies'; const TAB_COOKIES = "cookies";
const TAB_TIMELINE = 'timeline'; const TAB_TIMELINE = "timeline";
export type TimelineViewMode = 'timeline' | 'text'; export type TimelineViewMode = "timeline" | "text";
interface RedirectDropWarning { interface RedirectDropWarning {
droppedBodyCount: number; droppedBodyCount: number;
@@ -75,7 +75,7 @@ export function HttpResponsePane({ style, className, activeRequestId }: Props) {
[responseEvents.data], [responseEvents.data],
); );
const shouldShowRedirectDropWarning = const shouldShowRedirectDropWarning =
activeResponse?.state === 'closed' && redirectDropWarning != null; activeResponse?.state === "closed" && redirectDropWarning != null;
const cookieCounts = useMemo(() => getCookieCounts(responseEvents.data), [responseEvents.data]); const cookieCounts = useMemo(() => getCookieCounts(responseEvents.data), [responseEvents.data]);
@@ -83,27 +83,27 @@ export function HttpResponsePane({ style, className, activeRequestId }: Props) {
() => [ () => [
{ {
value: TAB_BODY, value: TAB_BODY,
label: 'Response', label: "Response",
options: { options: {
value: viewMode, value: viewMode,
onChange: setViewMode, onChange: setViewMode,
items: [ items: [
{ label: 'Response', value: 'pretty' }, { label: "Response", value: "pretty" },
...(mimeType?.startsWith('image') ...(mimeType?.startsWith("image")
? [] ? []
: [{ label: 'Response (Raw)', shortLabel: 'Raw', value: 'raw' }]), : [{ label: "Response (Raw)", shortLabel: "Raw", value: "raw" }]),
], ],
}, },
}, },
{ {
value: TAB_REQUEST, value: TAB_REQUEST,
label: 'Request', label: "Request",
rightSlot: rightSlot:
(activeResponse?.requestContentLength ?? 0) > 0 ? <CountBadge count={true} /> : null, (activeResponse?.requestContentLength ?? 0) > 0 ? <CountBadge count={true} /> : null,
}, },
{ {
value: TAB_HEADERS, value: TAB_HEADERS,
label: 'Headers', label: "Headers",
rightSlot: ( rightSlot: (
<CountBadge <CountBadge
count={activeResponse?.requestHeaders.length ?? 0} count={activeResponse?.requestHeaders.length ?? 0}
@@ -114,7 +114,7 @@ export function HttpResponsePane({ style, className, activeRequestId }: Props) {
}, },
{ {
value: TAB_COOKIES, value: TAB_COOKIES,
label: 'Cookies', label: "Cookies",
rightSlot: rightSlot:
cookieCounts.sent > 0 || cookieCounts.received > 0 ? ( cookieCounts.sent > 0 || cookieCounts.received > 0 ? (
<CountBadge count={cookieCounts.sent} count2={cookieCounts.received} showZero /> <CountBadge count={cookieCounts.sent} count2={cookieCounts.received} showZero />
@@ -125,10 +125,10 @@ export function HttpResponsePane({ style, className, activeRequestId }: Props) {
rightSlot: <CountBadge count={responseEvents.data?.length ?? 0} />, rightSlot: <CountBadge count={responseEvents.data?.length ?? 0} />,
options: { options: {
value: timelineViewMode, value: timelineViewMode,
onChange: (v) => setTimelineViewMode((v as TimelineViewMode) ?? 'timeline'), onChange: (v) => setTimelineViewMode((v as TimelineViewMode) ?? "timeline"),
items: [ items: [
{ label: 'Timeline', value: 'timeline' }, { label: "Timeline", value: "timeline" },
{ label: 'Timeline (Text)', shortLabel: 'Timeline', value: 'text' }, { label: "Timeline (Text)", shortLabel: "Timeline", value: "text" },
], ],
}, },
}, },
@@ -155,33 +155,33 @@ export function HttpResponsePane({ style, className, activeRequestId }: Props) {
style={style} style={style}
className={classNames( className={classNames(
className, className,
'x-theme-responsePane', "x-theme-responsePane",
'max-h-full h-full', "max-h-full h-full",
'bg-surface rounded-md border border-border-subtle overflow-hidden', "bg-surface rounded-md border border-border-subtle overflow-hidden",
'relative', "relative",
)} )}
> >
{activeResponse == null ? ( {activeResponse == null ? (
<HotkeyList hotkeys={['request.send', 'model.create', 'sidebar.focus', 'url_bar.focus']} /> <HotkeyList hotkeys={["request.send", "model.create", "sidebar.focus", "url_bar.focus"]} />
) : ( ) : (
<div className="h-full w-full grid grid-rows-[auto_minmax(0,1fr)] grid-cols-1"> <div className="h-full w-full grid grid-rows-[auto_minmax(0,1fr)] grid-cols-1">
<HStack <HStack
className={classNames( className={classNames(
'text-text-subtle w-full flex-shrink-0', "text-text-subtle w-full flex-shrink-0",
// Remove a bit of space because the tabs have lots too // Remove a bit of space because the tabs have lots too
'-mb-1.5', "-mb-1.5",
)} )}
> >
{activeResponse && ( {activeResponse && (
<div <div
className={classNames( className={classNames(
'grid grid-cols-[auto_minmax(4rem,1fr)_auto]', "grid grid-cols-[auto_minmax(4rem,1fr)_auto]",
'cursor-default select-none', "cursor-default select-none",
'whitespace-nowrap w-full pl-3 overflow-x-auto font-mono text-sm hide-scrollbars', "whitespace-nowrap w-full pl-3 overflow-x-auto font-mono text-sm hide-scrollbars",
)} )}
> >
<HStack space={2} className="w-full flex-shrink-0"> <HStack space={2} className="w-full flex-shrink-0">
{activeResponse.state !== 'closed' && <LoadingIcon size="sm" />} {activeResponse.state !== "closed" && <LoadingIcon size="sm" />}
<HttpStatusTag showReason response={activeResponse} /> <HttpStatusTag showReason response={activeResponse} />
<span>&bull;</span> <span>&bull;</span>
<HttpResponseDurationTag response={activeResponse} /> <HttpResponseDurationTag response={activeResponse} />
@@ -202,17 +202,17 @@ export function HttpResponsePane({ style, className, activeRequestId }: Props) {
</span> </span>
{redirectDropWarning.droppedBodyCount > 0 && ( {redirectDropWarning.droppedBodyCount > 0 && (
<span> <span>
Body dropped on {redirectDropWarning.droppedBodyCount}{' '} Body dropped on {redirectDropWarning.droppedBodyCount}{" "}
{redirectDropWarning.droppedBodyCount === 1 {redirectDropWarning.droppedBodyCount === 1
? 'redirect hop' ? "redirect hop"
: 'redirect hops'} : "redirect hops"}
</span> </span>
)} )}
{redirectDropWarning.droppedHeaders.length > 0 && ( {redirectDropWarning.droppedHeaders.length > 0 && (
<span> <span>
Headers dropped:{' '} Headers dropped:{" "}
<span className="font-mono"> <span className="font-mono">
{redirectDropWarning.droppedHeaders.join(', ')} {redirectDropWarning.droppedHeaders.join(", ")}
</span> </span>
</span> </span>
)} )}
@@ -266,7 +266,7 @@ export function HttpResponsePane({ style, className, activeRequestId }: Props) {
<ErrorBoundary name="Http Response Viewer"> <ErrorBoundary name="Http Response Viewer">
<Suspense> <Suspense>
<ConfirmLargeResponse response={activeResponse}> <ConfirmLargeResponse response={activeResponse}>
{activeResponse.state === 'initialized' ? ( {activeResponse.state === "initialized" ? (
<EmptyStateText> <EmptyStateText>
<VStack space={3}> <VStack space={3}>
<HStack space={3}> <HStack space={3}>
@@ -278,10 +278,10 @@ export function HttpResponsePane({ style, className, activeRequestId }: Props) {
</Button> </Button>
</VStack> </VStack>
</EmptyStateText> </EmptyStateText>
) : activeResponse.state === 'closed' && ) : activeResponse.state === "closed" &&
(activeResponse.contentLength ?? 0) === 0 ? ( (activeResponse.contentLength ?? 0) === 0 ? (
<EmptyStateText>Empty</EmptyStateText> <EmptyStateText>Empty</EmptyStateText>
) : mimeType?.match(/^text\/event-stream/i) && viewMode === 'pretty' ? ( ) : mimeType?.match(/^text\/event-stream/i) && viewMode === "pretty" ? (
<EventStreamViewer response={activeResponse} /> <EventStreamViewer response={activeResponse} />
) : mimeType?.match(/^image\/svg/) ? ( ) : mimeType?.match(/^image\/svg/) ? (
<HttpSvgViewer response={activeResponse} /> <HttpSvgViewer response={activeResponse} />
@@ -291,17 +291,17 @@ export function HttpResponsePane({ style, className, activeRequestId }: Props) {
<EnsureCompleteResponse response={activeResponse} Component={AudioViewer} /> <EnsureCompleteResponse response={activeResponse} Component={AudioViewer} />
) : mimeType?.match(/^video/i) ? ( ) : mimeType?.match(/^video/i) ? (
<EnsureCompleteResponse response={activeResponse} Component={VideoViewer} /> <EnsureCompleteResponse response={activeResponse} Component={VideoViewer} />
) : mimeType?.match(/^multipart/i) && viewMode === 'pretty' ? ( ) : mimeType?.match(/^multipart/i) && viewMode === "pretty" ? (
<HttpMultipartViewer response={activeResponse} /> <HttpMultipartViewer response={activeResponse} />
) : mimeType?.match(/pdf/i) ? ( ) : mimeType?.match(/pdf/i) ? (
<EnsureCompleteResponse response={activeResponse} Component={PdfViewer} /> <EnsureCompleteResponse response={activeResponse} Component={PdfViewer} />
) : mimeType?.match(/csv|tab-separated/i) && viewMode === 'pretty' ? ( ) : mimeType?.match(/csv|tab-separated/i) && viewMode === "pretty" ? (
<HttpCsvViewer className="pb-2" response={activeResponse} /> <HttpCsvViewer className="pb-2" response={activeResponse} />
) : ( ) : (
<HTMLOrTextViewer <HTMLOrTextViewer
textViewerClassName="-mr-2 bg-surface" // Pull to the right textViewerClassName="-mr-2 bg-surface" // Pull to the right
response={activeResponse} response={activeResponse}
pretty={viewMode === 'pretty'} pretty={viewMode === "pretty"}
/> />
)} )}
</ConfirmLargeResponse> </ConfirmLargeResponse>
@@ -339,7 +339,7 @@ function getRedirectDropWarning(
const droppedHeaders = new Set<string>(); const droppedHeaders = new Set<string>();
for (const e of events) { for (const e of events) {
const event = e.event; const event = e.event;
if (event.type !== 'redirect') { if (event.type !== "redirect") {
continue; continue;
} }
@@ -370,12 +370,12 @@ function pushHeaderName(headers: Set<string>, headerName: string): void {
function getRedirectWarningLabel(warning: RedirectDropWarning): string { function getRedirectWarningLabel(warning: RedirectDropWarning): string {
if (warning.droppedBodyCount > 0 && warning.droppedHeaders.length > 0) { if (warning.droppedBodyCount > 0 && warning.droppedHeaders.length > 0) {
return 'Dropped body and headers'; return "Dropped body and headers";
} }
if (warning.droppedBodyCount > 0) { if (warning.droppedBodyCount > 0) {
return 'Dropped body'; return "Dropped body";
} }
return 'Dropped headers'; return "Dropped headers";
} }
function EnsureCompleteResponse({ function EnsureCompleteResponse({
@@ -390,7 +390,7 @@ function EnsureCompleteResponse({
} }
// Wait until the response has been fully-downloaded // Wait until the response has been fully-downloaded
if (response.state !== 'closed') { if (response.state !== "closed") {
return ( return (
<EmptyStateText> <EmptyStateText>
<LoadingIcon /> <LoadingIcon />
@@ -421,7 +421,7 @@ function HttpMultipartViewer({ response }: { response: HttpResponse }) {
if (body.data == null) return null; if (body.data == null) return null;
const contentTypeHeader = getContentTypeFromHeaders(response.headers); const contentTypeHeader = getContentTypeFromHeaders(response.headers);
const boundary = contentTypeHeader?.split('boundary=')[1] ?? 'unknown'; const boundary = contentTypeHeader?.split("boundary=")[1] ?? "unknown";
return <MultipartViewer data={body.data} boundary={boundary} idPrefix={response.id} />; return <MultipartViewer data={body.data} boundary={boundary} idPrefix={response.id} />;
} }

View File

@@ -2,16 +2,16 @@ import type {
HttpResponse, HttpResponse,
HttpResponseEvent, HttpResponseEvent,
HttpResponseEventData, HttpResponseEventData,
} from '@yaakapp-internal/models'; } from "@yaakapp-internal/models";
import { type ReactNode, useMemo, useState } from 'react'; import { type ReactNode, useMemo, useState } from "react";
import { useHttpResponseEvents } from '../hooks/useHttpResponseEvents'; import { useHttpResponseEvents } from "../hooks/useHttpResponseEvents";
import { Editor } from './core/Editor/LazyEditor'; import { Editor } from "./core/Editor/LazyEditor";
import { type EventDetailAction, EventDetailHeader, EventViewer } from './core/EventViewer'; import { type EventDetailAction, EventDetailHeader, EventViewer } from "./core/EventViewer";
import { EventViewerRow } from './core/EventViewerRow'; import { EventViewerRow } from "./core/EventViewerRow";
import { HttpStatusTagRaw } from './core/HttpStatusTag'; import { HttpStatusTagRaw } from "./core/HttpStatusTag";
import { Icon, type IconProps } from '@yaakapp-internal/ui'; import { Icon, type IconProps } from "@yaakapp-internal/ui";
import { KeyValueRow, KeyValueRows } from './core/KeyValueRow'; import { KeyValueRow, KeyValueRows } from "./core/KeyValueRow";
import type { TimelineViewMode } from './HttpResponsePane'; import type { TimelineViewMode } from "./HttpResponsePane";
interface Props { interface Props {
response: HttpResponse; response: HttpResponse;
@@ -28,12 +28,12 @@ function Inner({ response, viewMode }: Props) {
// Generate plain text representation of all events (with prefixes for timeline view) // Generate plain text representation of all events (with prefixes for timeline view)
const plainText = useMemo(() => { const plainText = useMemo(() => {
if (!events || events.length === 0) return ''; if (!events || events.length === 0) return "";
return events.map((event) => formatEventText(event.event, true)).join('\n'); return events.map((event) => formatEventText(event.event, true)).join("\n");
}, [events]); }, [events]);
// Plain text view - show all events as text in an editor // Plain text view - show all events as text in an editor
if (viewMode === 'text') { if (viewMode === "text") {
if (isLoading) { if (isLoading) {
return <div className="p-4 text-text-subtlest">Loading events...</div>; return <div className="p-4 text-text-subtlest">Loading events...</div>;
} else if (error) { } else if (error) {
@@ -98,8 +98,8 @@ function EventDetails({
const actions: EventDetailAction[] = [ const actions: EventDetailAction[] = [
{ {
key: 'toggle-raw', key: "toggle-raw",
label: showRaw ? 'Formatted' : 'Text', label: showRaw ? "Formatted" : "Text",
onClick: () => setShowRaw(!showRaw), onClick: () => setShowRaw(!showRaw),
}, },
]; ];
@@ -107,24 +107,24 @@ function EventDetails({
// Determine the title based on event type // Determine the title based on event type
const title = (() => { const title = (() => {
switch (e.type) { switch (e.type) {
case 'header_up': case "header_up":
return 'Header Sent'; return "Header Sent";
case 'header_down': case "header_down":
return 'Header Received'; return "Header Received";
case 'send_url': case "send_url":
return 'Request'; return "Request";
case 'receive_url': case "receive_url":
return 'Response'; return "Response";
case 'redirect': case "redirect":
return 'Redirect'; return "Redirect";
case 'setting': case "setting":
return 'Apply Setting'; return "Apply Setting";
case 'chunk_sent': case "chunk_sent":
return 'Data Sent'; return "Data Sent";
case 'chunk_received': case "chunk_received":
return 'Data Received'; return "Data Received";
case 'dns_resolved': case "dns_resolved":
return e.overridden ? 'DNS Override' : 'DNS Resolution'; return e.overridden ? "DNS Override" : "DNS Resolution";
default: default:
return label; return label;
} }
@@ -139,7 +139,7 @@ function EventDetails({
} }
// Headers - show name and value // Headers - show name and value
if (e.type === 'header_up' || e.type === 'header_down') { if (e.type === "header_up" || e.type === "header_down") {
return ( return (
<KeyValueRows> <KeyValueRows>
<KeyValueRow label="Header">{e.name}</KeyValueRow> <KeyValueRow label="Header">{e.name}</KeyValueRow>
@@ -149,13 +149,13 @@ function EventDetails({
} }
// Request URL - show all URL parts separately // Request URL - show all URL parts separately
if (e.type === 'send_url') { if (e.type === "send_url") {
const auth = e.username || e.password ? `${e.username}:${e.password}@` : ''; const auth = e.username || e.password ? `${e.username}:${e.password}@` : "";
const isDefaultPort = const isDefaultPort =
(e.scheme === 'http' && e.port === 80) || (e.scheme === 'https' && e.port === 443); (e.scheme === "http" && e.port === 80) || (e.scheme === "https" && e.port === 443);
const portStr = isDefaultPort ? '' : `:${e.port}`; const portStr = isDefaultPort ? "" : `:${e.port}`;
const query = e.query ? `?${e.query}` : ''; const query = e.query ? `?${e.query}` : "";
const fragment = e.fragment ? `#${e.fragment}` : ''; const fragment = e.fragment ? `#${e.fragment}` : "";
const fullUrl = `${e.scheme}://${auth}${e.host}${portStr}${e.path}${query}${fragment}`; const fullUrl = `${e.scheme}://${auth}${e.host}${portStr}${e.path}${query}${fragment}`;
return ( return (
<KeyValueRows> <KeyValueRows>
@@ -174,7 +174,7 @@ function EventDetails({
} }
// Response status - show version and status separately // Response status - show version and status separately
if (e.type === 'receive_url') { if (e.type === "receive_url") {
return ( return (
<KeyValueRows> <KeyValueRows>
<KeyValueRow label="HTTP Version">{e.version}</KeyValueRow> <KeyValueRow label="HTTP Version">{e.version}</KeyValueRow>
@@ -186,7 +186,7 @@ function EventDetails({
} }
// Redirect - show status, URL, and behavior // Redirect - show status, URL, and behavior
if (e.type === 'redirect') { if (e.type === "redirect") {
const droppedHeaders = e.dropped_headers ?? []; const droppedHeaders = e.dropped_headers ?? [];
return ( return (
<KeyValueRows> <KeyValueRows>
@@ -195,18 +195,18 @@ function EventDetails({
</KeyValueRow> </KeyValueRow>
<KeyValueRow label="Location">{e.url}</KeyValueRow> <KeyValueRow label="Location">{e.url}</KeyValueRow>
<KeyValueRow label="Behavior"> <KeyValueRow label="Behavior">
{e.behavior === 'drop_body' ? 'Drop body, change to GET' : 'Preserve method and body'} {e.behavior === "drop_body" ? "Drop body, change to GET" : "Preserve method and body"}
</KeyValueRow> </KeyValueRow>
<KeyValueRow label="Body Dropped">{e.dropped_body ? 'Yes' : 'No'}</KeyValueRow> <KeyValueRow label="Body Dropped">{e.dropped_body ? "Yes" : "No"}</KeyValueRow>
<KeyValueRow label="Headers Dropped"> <KeyValueRow label="Headers Dropped">
{droppedHeaders.length > 0 ? droppedHeaders.join(', ') : '--'} {droppedHeaders.length > 0 ? droppedHeaders.join(", ") : "--"}
</KeyValueRow> </KeyValueRow>
</KeyValueRows> </KeyValueRows>
); );
} }
// Settings - show as key/value // Settings - show as key/value
if (e.type === 'setting') { if (e.type === "setting") {
return ( return (
<KeyValueRows> <KeyValueRows>
<KeyValueRow label="Setting">{e.name}</KeyValueRow> <KeyValueRow label="Setting">{e.name}</KeyValueRow>
@@ -216,16 +216,16 @@ function EventDetails({
} }
// Chunks - show formatted bytes // Chunks - show formatted bytes
if (e.type === 'chunk_sent' || e.type === 'chunk_received') { if (e.type === "chunk_sent" || e.type === "chunk_received") {
return <div className="font-mono text-editor">{formatBytes(e.bytes)}</div>; return <div className="font-mono text-editor">{formatBytes(e.bytes)}</div>;
} }
// DNS Resolution - show hostname, addresses, and timing // DNS Resolution - show hostname, addresses, and timing
if (e.type === 'dns_resolved') { if (e.type === "dns_resolved") {
return ( return (
<KeyValueRows> <KeyValueRows>
<KeyValueRow label="Hostname">{e.hostname}</KeyValueRow> <KeyValueRow label="Hostname">{e.hostname}</KeyValueRow>
<KeyValueRow label="Addresses">{e.addresses.join(', ')}</KeyValueRow> <KeyValueRow label="Addresses">{e.addresses.join(", ")}</KeyValueRow>
<KeyValueRow label="Duration"> <KeyValueRow label="Duration">
{e.overridden ? ( {e.overridden ? (
<span className="text-text-subtlest">--</span> <span className="text-text-subtlest">--</span>
@@ -255,57 +255,57 @@ function EventDetails({
); );
} }
type EventTextParts = { prefix: '>' | '<' | '*'; text: string }; type EventTextParts = { prefix: ">" | "<" | "*"; text: string };
/** Get the prefix and text for an event */ /** Get the prefix and text for an event */
function getEventTextParts(event: HttpResponseEventData): EventTextParts { function getEventTextParts(event: HttpResponseEventData): EventTextParts {
switch (event.type) { switch (event.type) {
case 'send_url': case "send_url":
return { return {
prefix: '>', prefix: ">",
text: `${event.method} ${event.path}${event.query ? `?${event.query}` : ''}${event.fragment ? `#${event.fragment}` : ''}`, text: `${event.method} ${event.path}${event.query ? `?${event.query}` : ""}${event.fragment ? `#${event.fragment}` : ""}`,
}; };
case 'receive_url': case "receive_url":
return { prefix: '<', text: `${event.version} ${event.status}` }; return { prefix: "<", text: `${event.version} ${event.status}` };
case 'header_up': case "header_up":
return { prefix: '>', text: `${event.name}: ${event.value}` }; return { prefix: ">", text: `${event.name}: ${event.value}` };
case 'header_down': case "header_down":
return { prefix: '<', text: `${event.name}: ${event.value}` }; return { prefix: "<", text: `${event.name}: ${event.value}` };
case 'redirect': { case "redirect": {
const behavior = event.behavior === 'drop_body' ? 'drop body' : 'preserve'; const behavior = event.behavior === "drop_body" ? "drop body" : "preserve";
const droppedHeaders = event.dropped_headers ?? []; const droppedHeaders = event.dropped_headers ?? [];
const dropped = [ const dropped = [
event.dropped_body ? 'body dropped' : null, event.dropped_body ? "body dropped" : null,
droppedHeaders.length > 0 ? `headers dropped: ${droppedHeaders.join(', ')}` : null, droppedHeaders.length > 0 ? `headers dropped: ${droppedHeaders.join(", ")}` : null,
] ]
.filter(Boolean) .filter(Boolean)
.join(', '); .join(", ");
return { return {
prefix: '*', prefix: "*",
text: `Redirect ${event.status} -> ${event.url} (${behavior}${dropped ? `, ${dropped}` : ''})`, text: `Redirect ${event.status} -> ${event.url} (${behavior}${dropped ? `, ${dropped}` : ""})`,
}; };
} }
case 'setting': case "setting":
return { prefix: '*', text: `Setting ${event.name}=${event.value}` }; return { prefix: "*", text: `Setting ${event.name}=${event.value}` };
case 'info': case "info":
return { prefix: '*', text: event.message }; return { prefix: "*", text: event.message };
case 'chunk_sent': case "chunk_sent":
return { prefix: '*', text: `[${formatBytes(event.bytes)} sent]` }; return { prefix: "*", text: `[${formatBytes(event.bytes)} sent]` };
case 'chunk_received': case "chunk_received":
return { prefix: '*', text: `[${formatBytes(event.bytes)} received]` }; return { prefix: "*", text: `[${formatBytes(event.bytes)} received]` };
case 'dns_resolved': case "dns_resolved":
if (event.overridden) { if (event.overridden) {
return { return {
prefix: '*', prefix: "*",
text: `DNS override ${event.hostname} -> ${event.addresses.join(', ')}`, text: `DNS override ${event.hostname} -> ${event.addresses.join(", ")}`,
}; };
} }
return { return {
prefix: '*', prefix: "*",
text: `DNS resolved ${event.hostname} to ${event.addresses.join(', ')} (${event.duration}ms)`, text: `DNS resolved ${event.hostname} to ${event.addresses.join(", ")} (${event.duration}ms)`,
}; };
default: default:
return { prefix: '*', text: '[unknown event]' }; return { prefix: "*", text: "[unknown event]" };
} }
} }
@@ -316,103 +316,103 @@ function formatEventText(event: HttpResponseEventData, includePrefix: boolean):
} }
type EventDisplay = { type EventDisplay = {
icon: IconProps['icon']; icon: IconProps["icon"];
color: IconProps['color']; color: IconProps["color"];
label: string; label: string;
summary: ReactNode; summary: ReactNode;
}; };
function getEventDisplay(event: HttpResponseEventData): EventDisplay { function getEventDisplay(event: HttpResponseEventData): EventDisplay {
switch (event.type) { switch (event.type) {
case 'setting': case "setting":
return { return {
icon: 'settings', icon: "settings",
color: 'secondary', color: "secondary",
label: 'Setting', label: "Setting",
summary: `${event.name} = ${event.value}`, summary: `${event.name} = ${event.value}`,
}; };
case 'info': case "info":
return { return {
icon: 'info', icon: "info",
color: 'secondary', color: "secondary",
label: 'Info', label: "Info",
summary: event.message, summary: event.message,
}; };
case 'redirect': { case "redirect": {
const droppedHeaders = event.dropped_headers ?? []; const droppedHeaders = event.dropped_headers ?? [];
const dropped = [ const dropped = [
event.dropped_body ? 'drop body' : null, event.dropped_body ? "drop body" : null,
droppedHeaders.length > 0 droppedHeaders.length > 0
? `drop ${droppedHeaders.length} ${droppedHeaders.length === 1 ? 'header' : 'headers'}` ? `drop ${droppedHeaders.length} ${droppedHeaders.length === 1 ? "header" : "headers"}`
: null, : null,
] ]
.filter(Boolean) .filter(Boolean)
.join(', '); .join(", ");
return { return {
icon: 'arrow_big_right_dash', icon: "arrow_big_right_dash",
color: 'success', color: "success",
label: 'Redirect', label: "Redirect",
summary: `Redirecting ${event.status} ${event.url}${dropped ? ` (${dropped})` : ''}`, summary: `Redirecting ${event.status} ${event.url}${dropped ? ` (${dropped})` : ""}`,
}; };
} }
case 'send_url': case "send_url":
return { return {
icon: 'arrow_big_up_dash', icon: "arrow_big_up_dash",
color: 'primary', color: "primary",
label: 'Request', label: "Request",
summary: `${event.method} ${event.path}${event.query ? `?${event.query}` : ''}${event.fragment ? `#${event.fragment}` : ''}`, summary: `${event.method} ${event.path}${event.query ? `?${event.query}` : ""}${event.fragment ? `#${event.fragment}` : ""}`,
}; };
case 'receive_url': case "receive_url":
return { return {
icon: 'arrow_big_down_dash', icon: "arrow_big_down_dash",
color: 'info', color: "info",
label: 'Response', label: "Response",
summary: `${event.version} ${event.status}`, summary: `${event.version} ${event.status}`,
}; };
case 'header_up': case "header_up":
return { return {
icon: 'arrow_big_up_dash', icon: "arrow_big_up_dash",
color: 'primary', color: "primary",
label: 'Header', label: "Header",
summary: `${event.name}: ${event.value}`, summary: `${event.name}: ${event.value}`,
}; };
case 'header_down': case "header_down":
return { return {
icon: 'arrow_big_down_dash', icon: "arrow_big_down_dash",
color: 'info', color: "info",
label: 'Header', label: "Header",
summary: `${event.name}: ${event.value}`, summary: `${event.name}: ${event.value}`,
}; };
case 'chunk_sent': case "chunk_sent":
return { return {
icon: 'info', icon: "info",
color: 'secondary', color: "secondary",
label: 'Chunk', label: "Chunk",
summary: `${formatBytes(event.bytes)} chunk sent`, summary: `${formatBytes(event.bytes)} chunk sent`,
}; };
case 'chunk_received': case "chunk_received":
return { return {
icon: 'info', icon: "info",
color: 'secondary', color: "secondary",
label: 'Chunk', label: "Chunk",
summary: `${formatBytes(event.bytes)} chunk received`, summary: `${formatBytes(event.bytes)} chunk received`,
}; };
case 'dns_resolved': case "dns_resolved":
return { return {
icon: 'globe', icon: "globe",
color: event.overridden ? 'success' : 'secondary', color: event.overridden ? "success" : "secondary",
label: event.overridden ? 'DNS Override' : 'DNS', label: event.overridden ? "DNS Override" : "DNS",
summary: event.overridden summary: event.overridden
? `${event.hostname}${event.addresses.join(', ')} (overridden)` ? `${event.hostname}${event.addresses.join(", ")} (overridden)`
: `${event.hostname}${event.addresses.join(', ')} (${event.duration}ms)`, : `${event.hostname}${event.addresses.join(", ")} (${event.duration}ms)`,
}; };
default: default:
return { return {
icon: 'info', icon: "info",
color: 'secondary', color: "secondary",
label: 'Unknown', label: "Unknown",
summary: 'Unknown event', summary: "Unknown event",
}; };
} }
} }

View File

@@ -1,14 +1,14 @@
import { clear, readText } from '@tauri-apps/plugin-clipboard-manager'; import { clear, readText } from "@tauri-apps/plugin-clipboard-manager";
import * as m from 'motion/react-m'; import * as m from "motion/react-m";
import { useEffect, useState } from 'react'; import { useEffect, useState } from "react";
import { useImportCurl } from '../hooks/useImportCurl'; import { useImportCurl } from "../hooks/useImportCurl";
import { useWindowFocus } from '../hooks/useWindowFocus'; import { useWindowFocus } from "../hooks/useWindowFocus";
import { Button } from './core/Button'; import { Button } from "./core/Button";
import { Icon } from '@yaakapp-internal/ui'; import { Icon } from "@yaakapp-internal/ui";
export function ImportCurlButton() { export function ImportCurlButton() {
const focused = useWindowFocus(); const focused = useWindowFocus();
const [clipboardText, setClipboardText] = useState(''); const [clipboardText, setClipboardText] = useState("");
const importCurl = useImportCurl(); const importCurl = useImportCurl();
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
@@ -18,7 +18,7 @@ export function ImportCurlButton() {
readText().then(setClipboardText); readText().then(setClipboardText);
}, [focused]); }, [focused]);
if (!clipboardText?.trim().startsWith('curl ')) { if (!clipboardText?.trim().startsWith("curl ")) {
return null; return null;
} }
@@ -41,9 +41,9 @@ export function ImportCurlButton() {
try { try {
await importCurl.mutateAsync({ command: clipboardText }); await importCurl.mutateAsync({ command: clipboardText });
await clear(); // Clear the clipboard so the button goes away await clear(); // Clear the clipboard so the button goes away
setClipboardText(''); setClipboardText("");
} catch (e) { } catch (e) {
console.log('Failed to import curl', e); console.log("Failed to import curl", e);
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }

View File

@@ -1,8 +1,8 @@
import { VStack } from '@yaakapp-internal/ui'; import { VStack } from "@yaakapp-internal/ui";
import { useState } from 'react'; import { useState } from "react";
import { useLocalStorage } from 'react-use'; import { useLocalStorage } from "react-use";
import { Button } from './core/Button'; import { Button } from "./core/Button";
import { SelectFile } from './SelectFile'; import { SelectFile } from "./SelectFile";
interface Props { interface Props {
importData: (filePath: string) => Promise<void>; importData: (filePath: string) => Promise<void>;
@@ -10,7 +10,7 @@ interface Props {
export function ImportDataDialog({ importData }: Props) { export function ImportDataDialog({ importData }: Props) {
const [isLoading, setIsLoading] = useState<boolean>(false); const [isLoading, setIsLoading] = useState<boolean>(false);
const [filePath, setFilePath] = useLocalStorage<string | null>('importFilePath', null); const [filePath, setFilePath] = useLocalStorage<string | null>("importFilePath", null);
return ( return (
<VStack space={5} className="pb-4"> <VStack space={5} className="pb-4">
@@ -45,7 +45,7 @@ export function ImportDataDialog({ importData }: Props) {
} }
}} }}
> >
{isLoading ? 'Importing' : 'Import'} {isLoading ? "Importing" : "Import"}
</Button> </Button>
)} )}
</VStack> </VStack>

View File

@@ -1,5 +1,5 @@
import type { ReactNode } from 'react'; import type { ReactNode } from "react";
import { appInfo } from '../lib/appInfo'; import { appInfo } from "../lib/appInfo";
interface Props { interface Props {
children: ReactNode; children: ReactNode;

View File

@@ -1,21 +1,21 @@
import { linter } from '@codemirror/lint'; import { linter } from "@codemirror/lint";
import type { HttpRequest } from '@yaakapp-internal/models'; import type { HttpRequest } from "@yaakapp-internal/models";
import { patchModel } from '@yaakapp-internal/models'; import { patchModel } from "@yaakapp-internal/models";
import { Banner, Icon } from '@yaakapp-internal/ui'; import { Banner, Icon } from "@yaakapp-internal/ui";
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from "react";
import { useKeyValue } from '../hooks/useKeyValue'; import { useKeyValue } from "../hooks/useKeyValue";
import { textLikelyContainsJsonComments } from '../lib/jsonComments'; import { textLikelyContainsJsonComments } from "../lib/jsonComments";
import type { DropdownItem } from './core/Dropdown'; import type { DropdownItem } from "./core/Dropdown";
import { Dropdown } from './core/Dropdown'; import { Dropdown } from "./core/Dropdown";
import type { EditorProps } from './core/Editor/Editor'; import type { EditorProps } from "./core/Editor/Editor";
import { jsonParseLinter } from './core/Editor/json-lint'; import { jsonParseLinter } from "./core/Editor/json-lint";
import { Editor } from './core/Editor/LazyEditor'; import { Editor } from "./core/Editor/LazyEditor";
import { IconButton } from './core/IconButton'; import { IconButton } from "./core/IconButton";
import { IconTooltip } from './core/IconTooltip'; import { IconTooltip } from "./core/IconTooltip";
interface Props { interface Props {
forceUpdateKey: string; forceUpdateKey: string;
heightMode: EditorProps['heightMode']; heightMode: EditorProps["heightMode"];
request: HttpRequest; request: HttpRequest;
} }
@@ -40,13 +40,13 @@ export function JsonBodyEditor({ forceUpdateKey, heightMode, request }: Props) {
); );
const hasComments = useMemo( const hasComments = useMemo(
() => textLikelyContainsJsonComments(request.body?.text ?? ''), () => textLikelyContainsJsonComments(request.body?.text ?? ""),
[request.body?.text], [request.body?.text],
); );
const { value: bannerDismissed, set: setBannerDismissed } = useKeyValue<boolean>({ const { value: bannerDismissed, set: setBannerDismissed } = useKeyValue<boolean>({
namespace: 'no_sync', namespace: "no_sync",
key: ['json-fix-3', request.workspaceId], key: ["json-fix-3", request.workspaceId],
fallback: false, fallback: false,
}); });
@@ -68,8 +68,8 @@ export function JsonBodyEditor({ forceUpdateKey, heightMode, request }: Props) {
const showBanner = hasComments && autoFix && !bannerDismissed; const showBanner = hasComments && autoFix && !bannerDismissed;
const stripMessage = 'Automatically strip comments and trailing commas before sending'; const stripMessage = "Automatically strip comments and trailing commas before sending";
const actions = useMemo<EditorProps['actions']>( const actions = useMemo<EditorProps["actions"]>(
() => [ () => [
showBanner && ( showBanner && (
<Banner color="notice" className="!opacity-100 h-sm !py-0 !px-2 flex items-center text-xs"> <Banner color="notice" className="!opacity-100 h-sm !py-0 !px-2 flex items-center text-xs">
@@ -85,12 +85,12 @@ export function JsonBodyEditor({ forceUpdateKey, heightMode, request }: Props) {
items={ items={
[ [
{ {
label: 'Automatically Fix JSON', label: "Automatically Fix JSON",
keepOpenOnSelect: true, keepOpenOnSelect: true,
onSelect: handleToggleAutoFix, onSelect: handleToggleAutoFix,
rightSlot: <IconTooltip content={stripMessage} />, rightSlot: <IconTooltip content={stripMessage} />,
leftSlot: ( leftSlot: (
<Icon icon={autoFix ? 'check_square_checked' : 'check_square_unchecked'} /> <Icon icon={autoFix ? "check_square_checked" : "check_square_unchecked"} />
), ),
}, },
] satisfies DropdownItem[] ] satisfies DropdownItem[]
@@ -110,7 +110,7 @@ export function JsonBodyEditor({ forceUpdateKey, heightMode, request }: Props) {
autocompleteVariables autocompleteVariables
placeholder="..." placeholder="..."
heightMode={heightMode} heightMode={heightMode}
defaultValue={`${request.body?.text ?? ''}`} defaultValue={`${request.body?.text ?? ""}`}
language="json" language="json"
onChange={handleChange} onChange={handleChange}
stateKey={`json.${request.id}`} stateKey={`json.${request.id}`}

View File

@@ -1,5 +1,5 @@
import { hotkeyActions } from '../hooks/useHotKey'; import { hotkeyActions } from "../hooks/useHotKey";
import { HotkeyList } from './core/HotkeyList'; import { HotkeyList } from "./core/HotkeyList";
export function KeyboardShortcutsDialog() { export function KeyboardShortcutsDialog() {
return ( return (

View File

@@ -1,62 +1,62 @@
import { openUrl } from '@tauri-apps/plugin-opener'; import { openUrl } from "@tauri-apps/plugin-opener";
import type { LicenseCheckStatus } from '@yaakapp-internal/license'; import type { LicenseCheckStatus } from "@yaakapp-internal/license";
import { useLicense } from '@yaakapp-internal/license'; import { useLicense } from "@yaakapp-internal/license";
import { settingsAtom } from '@yaakapp-internal/models'; import { settingsAtom } from "@yaakapp-internal/models";
import { differenceInCalendarDays } from 'date-fns'; import { differenceInCalendarDays } from "date-fns";
import { formatDate } from 'date-fns/format'; import { formatDate } from "date-fns/format";
import { useAtomValue } from 'jotai'; import { useAtomValue } from "jotai";
import type { ReactNode } from 'react'; import type { ReactNode } from "react";
import { openSettings } from '../commands/openSettings'; import { openSettings } from "../commands/openSettings";
import { atomWithKVStorage } from '../lib/atoms/atomWithKVStorage'; import { atomWithKVStorage } from "../lib/atoms/atomWithKVStorage";
import { jotaiStore } from '../lib/jotai'; import { jotaiStore } from "../lib/jotai";
import { CargoFeature } from './CargoFeature'; import { CargoFeature } from "./CargoFeature";
import type { ButtonProps } from './core/Button'; import type { ButtonProps } from "./core/Button";
import { Dropdown, type DropdownItem } from './core/Dropdown'; import { Dropdown, type DropdownItem } from "./core/Dropdown";
import { Icon } from '@yaakapp-internal/ui'; import { Icon } from "@yaakapp-internal/ui";
import { PillButton } from './core/PillButton'; import { PillButton } from "./core/PillButton";
const dismissedAtom = atomWithKVStorage<string | null>('dismissed_license_expired', null); const dismissedAtom = atomWithKVStorage<string | null>("dismissed_license_expired", null);
function getDetail( function getDetail(
data: LicenseCheckStatus, data: LicenseCheckStatus,
dismissedExpired: string | null, dismissedExpired: string | null,
): { label: ReactNode; color: ButtonProps['color']; options?: DropdownItem[] } | null | undefined { ): { label: ReactNode; color: ButtonProps["color"]; options?: DropdownItem[] } | null | undefined {
const dismissedAt = dismissedExpired ? new Date(dismissedExpired).getTime() : null; const dismissedAt = dismissedExpired ? new Date(dismissedExpired).getTime() : null;
switch (data.status) { switch (data.status) {
case 'active': case "active":
return null; return null;
case 'personal_use': case "personal_use":
return { label: 'Personal Use', color: 'notice' }; return { label: "Personal Use", color: "notice" };
case 'trialing': case "trialing":
return { label: 'Commercial Trial', color: 'secondary' }; return { label: "Commercial Trial", color: "secondary" };
case 'error': case "error":
return { label: 'Error', color: 'danger' }; return { label: "Error", color: "danger" };
case 'inactive': case "inactive":
return { label: 'Personal Use', color: 'notice' }; return { label: "Personal Use", color: "notice" };
case 'past_due': case "past_due":
return { label: 'Past Due', color: 'danger' }; return { label: "Past Due", color: "danger" };
case 'expired': case "expired":
// Don't show the expired message if it's been less than 14 days since the last dismissal // Don't show the expired message if it's been less than 14 days since the last dismissal
if (dismissedAt && differenceInCalendarDays(new Date(), dismissedAt) < 14) { if (dismissedAt && differenceInCalendarDays(new Date(), dismissedAt) < 14) {
return null; return null;
} }
return { return {
color: 'notice', color: "notice",
label: data.data.changes > 0 ? 'Updates Paused' : 'License Expired', label: data.data.changes > 0 ? "Updates Paused" : "License Expired",
options: [ options: [
{ {
label: `${data.data.changes} New Updates`, label: `${data.data.changes} New Updates`,
color: 'success', color: "success",
leftSlot: <Icon icon="gift" />, leftSlot: <Icon icon="gift" />,
rightSlot: <Icon icon="external_link" size="sm" className="opacity-disabled" />, rightSlot: <Icon icon="external_link" size="sm" className="opacity-disabled" />,
hidden: data.data.changes === 0 || data.data.changesUrl == null, hidden: data.data.changes === 0 || data.data.changesUrl == null,
onSelect: () => openUrl(data.data.changesUrl ?? ''), onSelect: () => openUrl(data.data.changesUrl ?? ""),
}, },
{ {
type: 'separator', type: "separator",
label: `License expired ${formatDate(data.data.periodEnd, 'MMM dd, yyyy')}`, label: `License expired ${formatDate(data.data.periodEnd, "MMM dd, yyyy")}`,
}, },
{ {
label: <div className="min-w-[12rem]">Renew License</div>, label: <div className="min-w-[12rem]">Renew License</div>,
@@ -66,12 +66,12 @@ function getDetail(
onSelect: () => openUrl(data.data.billingUrl), onSelect: () => openUrl(data.data.billingUrl),
}, },
{ {
label: 'Enter License Key', label: "Enter License Key",
leftSlot: <Icon icon="key_round" />, leftSlot: <Icon icon="key_round" />,
hidden: data.data.changesUrl == null, hidden: data.data.changesUrl == null,
onSelect: openLicenseDialog, onSelect: openLicenseDialog,
}, },
{ type: 'separator' }, { type: "separator" },
{ {
label: <span className="text-text-subtle">Remind me Later</span>, label: <span className="text-text-subtle">Remind me Later</span>,
leftSlot: <Icon icon="alarm_clock" className="text-text-subtle" />, leftSlot: <Icon icon="alarm_clock" className="text-text-subtle" />,
@@ -135,5 +135,5 @@ function LicenseBadgeCmp() {
} }
function openLicenseDialog() { function openLicenseDialog() {
openSettings.mutate('license'); openSettings.mutate("license");
} }

View File

@@ -1,7 +1,7 @@
import { useQuery } from '@tanstack/react-query'; import { useQuery } from "@tanstack/react-query";
import { convertFileSrc } from '@tauri-apps/api/core'; import { convertFileSrc } from "@tauri-apps/api/core";
import { resolveResource } from '@tauri-apps/api/path'; import { resolveResource } from "@tauri-apps/api/path";
import classNames from 'classnames'; import classNames from "classnames";
interface Props { interface Props {
src: string; src: string;
@@ -10,7 +10,7 @@ interface Props {
export function LocalImage({ src: srcPath, className }: Props) { export function LocalImage({ src: srcPath, className }: Props) {
const src = useQuery({ const src = useQuery({
queryKey: ['local-image', srcPath], queryKey: ["local-image", srcPath],
queryFn: async () => { queryFn: async () => {
const p = await resolveResource(srcPath); const p = await resolveResource(srcPath);
return convertFileSrc(p); return convertFileSrc(p);
@@ -23,8 +23,8 @@ export function LocalImage({ src: srcPath, className }: Props) {
alt="Response preview" alt="Response preview"
className={classNames( className={classNames(
className, className,
'transition-opacity', "transition-opacity",
src.data == null ? 'opacity-0' : 'opacity-100', src.data == null ? "opacity-0" : "opacity-100",
)} )}
/> />
); );

View File

@@ -1,9 +1,9 @@
import type { CSSProperties } from 'react'; import type { CSSProperties } from "react";
import ReactMarkdown, { type Components } from 'react-markdown'; import ReactMarkdown, { type Components } from "react-markdown";
import { PrismLight as SyntaxHighlighter } from 'react-syntax-highlighter'; import { PrismLight as SyntaxHighlighter } from "react-syntax-highlighter";
import remarkGfm from 'remark-gfm'; import remarkGfm from "remark-gfm";
import { ErrorBoundary } from './ErrorBoundary'; import { ErrorBoundary } from "./ErrorBoundary";
import { Prose } from './Prose'; import { Prose } from "./Prose";
interface Props { interface Props {
children: string | null; children: string | null;
@@ -30,48 +30,48 @@ const prismTheme = {
}, },
// Syntax tokens // Syntax tokens
comment: { color: 'var(--textSubtle)' }, comment: { color: "var(--textSubtle)" },
prolog: { color: 'var(--textSubtle)' }, prolog: { color: "var(--textSubtle)" },
doctype: { color: 'var(--textSubtle)' }, doctype: { color: "var(--textSubtle)" },
cdata: { color: 'var(--textSubtle)' }, cdata: { color: "var(--textSubtle)" },
punctuation: { color: 'var(--textSubtle)' }, punctuation: { color: "var(--textSubtle)" },
property: { color: 'var(--primary)' }, property: { color: "var(--primary)" },
'attr-name': { color: 'var(--primary)' }, "attr-name": { color: "var(--primary)" },
string: { color: 'var(--notice)' }, string: { color: "var(--notice)" },
char: { color: 'var(--notice)' }, char: { color: "var(--notice)" },
number: { color: 'var(--info)' }, number: { color: "var(--info)" },
constant: { color: 'var(--info)' }, constant: { color: "var(--info)" },
symbol: { color: 'var(--info)' }, symbol: { color: "var(--info)" },
boolean: { color: 'var(--warning)' }, boolean: { color: "var(--warning)" },
'attr-value': { color: 'var(--warning)' }, "attr-value": { color: "var(--warning)" },
variable: { color: 'var(--success)' }, variable: { color: "var(--success)" },
tag: { color: 'var(--info)' }, tag: { color: "var(--info)" },
operator: { color: 'var(--danger)' }, operator: { color: "var(--danger)" },
keyword: { color: 'var(--danger)' }, keyword: { color: "var(--danger)" },
function: { color: 'var(--success)' }, function: { color: "var(--success)" },
'class-name': { color: 'var(--primary)' }, "class-name": { color: "var(--primary)" },
builtin: { color: 'var(--danger)' }, builtin: { color: "var(--danger)" },
selector: { color: 'var(--danger)' }, selector: { color: "var(--danger)" },
inserted: { color: 'var(--success)' }, inserted: { color: "var(--success)" },
deleted: { color: 'var(--danger)' }, deleted: { color: "var(--danger)" },
regex: { color: 'var(--warning)' }, regex: { color: "var(--warning)" },
important: { color: 'var(--danger)', fontWeight: 'bold' }, important: { color: "var(--danger)", fontWeight: "bold" },
italic: { fontStyle: 'italic' }, italic: { fontStyle: "italic" },
bold: { fontWeight: 'bold' }, bold: { fontWeight: "bold" },
entity: { cursor: 'help' }, entity: { cursor: "help" },
}; };
const lineStyle: CSSProperties = { const lineStyle: CSSProperties = {
paddingRight: '1.5em', paddingRight: "1.5em",
paddingLeft: '0', paddingLeft: "0",
opacity: 0.5, opacity: 0.5,
}; };
@@ -91,7 +91,7 @@ const markdownComponents: Partial<Components> = {
const { children, className, ref, ...extraProps } = props; const { children, className, ref, ...extraProps } = props;
extraProps.node = undefined; extraProps.node = undefined;
const match = /language-(\w+)/.exec(className || ''); const match = /language-(\w+)/.exec(className || "");
return match ? ( return match ? (
<SyntaxHighlighter <SyntaxHighlighter
{...extraProps} {...extraProps}
@@ -102,7 +102,7 @@ const markdownComponents: Partial<Components> = {
language={match[1]} language={match[1]}
style={prismTheme} style={prismTheme}
> >
{String(children).replace(/\n$/, '')} {String(children as string).replace(/\n$/, "")}
</SyntaxHighlighter> </SyntaxHighlighter>
) : ( ) : (
<code {...extraProps} ref={ref} className={className}> <code {...extraProps} ref={ref} className={className}>

View File

@@ -1,13 +1,13 @@
import classNames from 'classnames'; import classNames from "classnames";
import { useRef, useState } from 'react'; import { useRef, useState } from "react";
import type { EditorProps } from './core/Editor/Editor'; import type { EditorProps } from "./core/Editor/Editor";
import { Editor } from './core/Editor/LazyEditor'; import { Editor } from "./core/Editor/LazyEditor";
import { SegmentedControl } from './core/SegmentedControl'; import { SegmentedControl } from "./core/SegmentedControl";
import { Markdown } from './Markdown'; import { Markdown } from "./Markdown";
type ViewMode = 'edit' | 'preview'; type ViewMode = "edit" | "preview";
interface Props extends Pick<EditorProps, 'heightMode' | 'stateKey' | 'forceUpdateKey'> { interface Props extends Pick<EditorProps, "heightMode" | "stateKey" | "forceUpdateKey"> {
placeholder: string; placeholder: string;
className?: string; className?: string;
editorClassName?: string; editorClassName?: string;
@@ -25,7 +25,7 @@ export function MarkdownEditor({
forceUpdateKey, forceUpdateKey,
...editorProps ...editorProps
}: Props) { }: Props) {
const [viewMode, setViewMode] = useState<ViewMode>(defaultValue ? 'preview' : 'edit'); const [viewMode, setViewMode] = useState<ViewMode>(defaultValue ? "preview" : "edit");
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
@@ -33,7 +33,7 @@ export function MarkdownEditor({
<Editor <Editor
hideGutter hideGutter
wrapLines wrapLines
className={classNames(editorClassName, '[&_.cm-line]:!max-w-lg max-h-full')} className={classNames(editorClassName, "[&_.cm-line]:!max-w-lg max-h-full")}
language="markdown" language="markdown"
defaultValue={defaultValue} defaultValue={defaultValue}
onChange={onChange} onChange={onChange}
@@ -51,15 +51,15 @@ export function MarkdownEditor({
</div> </div>
); );
const contents = viewMode === 'preview' ? preview : editor; const contents = viewMode === "preview" ? preview : editor;
return ( return (
<div <div
ref={containerRef} ref={containerRef}
className={classNames( className={classNames(
'group/markdown', "group/markdown",
'relative w-full h-full pt-1.5 rounded-md gap-x-1.5', "relative w-full h-full pt-1.5 rounded-md gap-x-1.5",
'min-w-0', // Not sure why this is needed "min-w-0", // Not sure why this is needed
className, className,
)} )}
> >
@@ -73,8 +73,8 @@ export function MarkdownEditor({
value={viewMode} value={viewMode}
className="opacity-0 group-focus-within/markdown:opacity-100 group-hover/markdown:opacity-100" className="opacity-0 group-focus-within/markdown:opacity-100 group-hover/markdown:opacity-100"
options={[ options={[
{ icon: 'eye', label: 'Preview mode', value: 'preview' }, { icon: "eye", label: "Preview mode", value: "preview" },
{ icon: 'pencil', label: 'Edit mode', value: 'edit' }, { icon: "pencil", label: "Edit mode", value: "edit" },
]} ]}
/> />
</div> </div>

View File

@@ -1,14 +1,14 @@
import type { GrpcRequest, HttpRequest, WebsocketRequest } from '@yaakapp-internal/models'; import type { GrpcRequest, HttpRequest, WebsocketRequest } from "@yaakapp-internal/models";
import { patchModel, workspacesAtom } from '@yaakapp-internal/models'; import { patchModel, workspacesAtom } from "@yaakapp-internal/models";
import { InlineCode, VStack } from '@yaakapp-internal/ui'; import { InlineCode, VStack } from "@yaakapp-internal/ui";
import { useAtomValue } from 'jotai'; import { useAtomValue } from "jotai";
import { useState } from 'react'; import { useState } from "react";
import { pluralizeCount } from '../lib/pluralize'; import { pluralizeCount } from "../lib/pluralize";
import { resolvedModelName } from '../lib/resolvedModelName'; import { resolvedModelName } from "../lib/resolvedModelName";
import { router } from '../lib/router'; import { router } from "../lib/router";
import { showToast } from '../lib/toast'; import { showToast } from "../lib/toast";
import { Button } from './core/Button'; import { Button } from "./core/Button";
import { Select } from './core/Select'; import { Select } from "./core/Select";
interface Props { interface Props {
activeWorkspaceId: string; activeWorkspaceId: string;
@@ -49,17 +49,17 @@ export function MoveToWorkspaceDialog({ onDone, requests, activeWorkspaceId }: P
// Hide after a moment, to give time for requests to disappear // Hide after a moment, to give time for requests to disappear
setTimeout(onDone, 100); setTimeout(onDone, 100);
showToast({ showToast({
id: 'workspace-moved', id: "workspace-moved",
message: message:
requests.length === 1 && requests[0] != null ? ( requests.length === 1 && requests[0] != null ? (
<> <>
<InlineCode>{resolvedModelName(requests[0])}</InlineCode> moved to{' '} <InlineCode>{resolvedModelName(requests[0])}</InlineCode> moved to{" "}
<InlineCode>{targetWorkspace?.name ?? 'unknown'}</InlineCode> <InlineCode>{targetWorkspace?.name ?? "unknown"}</InlineCode>
</> </>
) : ( ) : (
<> <>
{pluralizeCount('request', requests.length)} moved to{' '} {pluralizeCount("request", requests.length)} moved to{" "}
<InlineCode>{targetWorkspace?.name ?? 'unknown'}</InlineCode> <InlineCode>{targetWorkspace?.name ?? "unknown"}</InlineCode>
</> </>
), ),
action: ({ hide }) => ( action: ({ hide }) => (
@@ -69,7 +69,7 @@ export function MoveToWorkspaceDialog({ onDone, requests, activeWorkspaceId }: P
className="mr-auto min-w-[5rem]" className="mr-auto min-w-[5rem]"
onClick={async () => { onClick={async () => {
await router.navigate({ await router.navigate({
to: '/workspaces/$workspaceId', to: "/workspaces/$workspaceId",
params: { workspaceId: selectedWorkspaceId }, params: { workspaceId: selectedWorkspaceId },
}); });
hide(); hide();
@@ -81,7 +81,7 @@ export function MoveToWorkspaceDialog({ onDone, requests, activeWorkspaceId }: P
}); });
}} }}
> >
{requests.length === 1 ? 'Move' : `Move ${pluralizeCount('Request', requests.length)}`} {requests.length === 1 ? "Move" : `Move ${pluralizeCount("Request", requests.length)}`}
</Button> </Button>
</VStack> </VStack>
); );

View File

@@ -1,6 +1,6 @@
import classNames from 'classnames'; import classNames from "classnames";
import type { ReactNode } from 'react'; import type { ReactNode } from "react";
import './Prose.css'; import "./Prose.css";
interface Props { interface Props {
children: ReactNode; children: ReactNode;
@@ -8,5 +8,5 @@ interface Props {
} }
export function Prose({ className, ...props }: Props) { export function Prose({ className, ...props }: Props) {
return <div className={classNames('prose', className)} {...props} />; return <div className={classNames("prose", className)} {...props} />;
} }

View File

@@ -1,11 +1,11 @@
import type { GrpcConnection } from '@yaakapp-internal/models'; import type { GrpcConnection } from "@yaakapp-internal/models";
import { deleteModel } from '@yaakapp-internal/models'; import { deleteModel } from "@yaakapp-internal/models";
import { HStack, Icon } from '@yaakapp-internal/ui'; import { HStack, Icon } from "@yaakapp-internal/ui";
import { formatDistanceToNowStrict } from 'date-fns'; import { formatDistanceToNowStrict } from "date-fns";
import { useDeleteGrpcConnections } from '../hooks/useDeleteGrpcConnections'; import { useDeleteGrpcConnections } from "../hooks/useDeleteGrpcConnections";
import { pluralizeCount } from '../lib/pluralize'; import { pluralizeCount } from "../lib/pluralize";
import { Dropdown } from './core/Dropdown'; import { Dropdown } from "./core/Dropdown";
import { IconButton } from './core/IconButton'; import { IconButton } from "./core/IconButton";
interface Props { interface Props {
connections: GrpcConnection[]; connections: GrpcConnection[];
@@ -19,27 +19,27 @@ export function RecentGrpcConnectionsDropdown({
onPinnedConnectionId, onPinnedConnectionId,
}: Props) { }: Props) {
const deleteAllConnections = useDeleteGrpcConnections(activeConnection?.requestId); const deleteAllConnections = useDeleteGrpcConnections(activeConnection?.requestId);
const latestConnectionId = connections[0]?.id ?? 'n/a'; const latestConnectionId = connections[0]?.id ?? "n/a";
return ( return (
<Dropdown <Dropdown
items={[ items={[
{ {
label: 'Clear Connection', label: "Clear Connection",
onSelect: () => deleteModel(activeConnection), onSelect: () => deleteModel(activeConnection),
disabled: connections.length === 0, disabled: connections.length === 0,
}, },
{ {
label: `Clear ${pluralizeCount('Connection', connections.length)}`, label: `Clear ${pluralizeCount("Connection", connections.length)}`,
onSelect: deleteAllConnections.mutate, onSelect: deleteAllConnections.mutate,
hidden: connections.length <= 1, hidden: connections.length <= 1,
disabled: connections.length === 0, disabled: connections.length === 0,
}, },
{ type: 'separator', label: 'History' }, { type: "separator", label: "History" },
...connections.map((c) => ({ ...connections.map((c) => ({
label: ( label: (
<HStack space={2}> <HStack space={2}>
{formatDistanceToNowStrict(`${c.createdAt}Z`)} ago &bull;{' '} {formatDistanceToNowStrict(`${c.createdAt}Z`)} ago &bull;{" "}
<span className="font-mono text-sm">{c.elapsed}ms</span> <span className="font-mono text-sm">{c.elapsed}ms</span>
</HStack> </HStack>
), ),
@@ -50,7 +50,7 @@ export function RecentGrpcConnectionsDropdown({
> >
<IconButton <IconButton
title="Show connection history" title="Show connection history"
icon={activeConnection?.id === latestConnectionId ? 'history' : 'pin'} icon={activeConnection?.id === latestConnectionId ? "history" : "pin"}
className="m-0.5 text-text-subtle" className="m-0.5 text-text-subtle"
size="sm" size="sm"
iconSize="md" iconSize="md"

View File

@@ -1,13 +1,13 @@
import type { HttpResponse } from '@yaakapp-internal/models'; import type { HttpResponse } from "@yaakapp-internal/models";
import { deleteModel } from '@yaakapp-internal/models'; import { deleteModel } from "@yaakapp-internal/models";
import { HStack, Icon } from '@yaakapp-internal/ui'; import { HStack, Icon } from "@yaakapp-internal/ui";
import { useCopyHttpResponse } from '../hooks/useCopyHttpResponse'; import { useCopyHttpResponse } from "../hooks/useCopyHttpResponse";
import { useDeleteHttpResponses } from '../hooks/useDeleteHttpResponses'; import { useDeleteHttpResponses } from "../hooks/useDeleteHttpResponses";
import { useSaveResponse } from '../hooks/useSaveResponse'; import { useSaveResponse } from "../hooks/useSaveResponse";
import { pluralize } from '../lib/pluralize'; import { pluralize } from "../lib/pluralize";
import { Dropdown } from './core/Dropdown'; import { Dropdown } from "./core/Dropdown";
import { HttpStatusTag } from './core/HttpStatusTag'; import { HttpStatusTag } from "./core/HttpStatusTag";
import { IconButton } from './core/IconButton'; import { IconButton } from "./core/IconButton";
interface Props { interface Props {
responses: HttpResponse[]; responses: HttpResponse[];
@@ -22,7 +22,7 @@ export const RecentHttpResponsesDropdown = function ResponsePane({
onPinnedResponseId, onPinnedResponseId,
}: Props) { }: Props) {
const deleteAllResponses = useDeleteHttpResponses(activeResponse?.requestId); const deleteAllResponses = useDeleteHttpResponses(activeResponse?.requestId);
const latestResponseId = responses[0]?.id ?? 'n/a'; const latestResponseId = responses[0]?.id ?? "n/a";
const saveResponse = useSaveResponse(activeResponse); const saveResponse = useSaveResponse(activeResponse);
const copyResponse = useCopyHttpResponse(activeResponse); const copyResponse = useCopyHttpResponse(activeResponse);
@@ -30,45 +30,45 @@ export const RecentHttpResponsesDropdown = function ResponsePane({
<Dropdown <Dropdown
items={[ items={[
{ {
label: 'Save to File', label: "Save to File",
onSelect: saveResponse.mutate, onSelect: saveResponse.mutate,
leftSlot: <Icon icon="save" />, leftSlot: <Icon icon="save" />,
hidden: responses.length === 0 || !!activeResponse.error, hidden: responses.length === 0 || !!activeResponse.error,
disabled: activeResponse.state !== 'closed' && activeResponse.status >= 100, disabled: activeResponse.state !== "closed" && activeResponse.status >= 100,
}, },
{ {
label: 'Copy Body', label: "Copy Body",
onSelect: copyResponse.mutate, onSelect: copyResponse.mutate,
leftSlot: <Icon icon="copy" />, leftSlot: <Icon icon="copy" />,
hidden: responses.length === 0 || !!activeResponse.error, hidden: responses.length === 0 || !!activeResponse.error,
disabled: activeResponse.state !== 'closed' && activeResponse.status >= 100, disabled: activeResponse.state !== "closed" && activeResponse.status >= 100,
}, },
{ {
label: 'Delete', label: "Delete",
leftSlot: <Icon icon="trash" />, leftSlot: <Icon icon="trash" />,
onSelect: () => deleteModel(activeResponse), onSelect: () => deleteModel(activeResponse),
}, },
{ {
label: 'Unpin Response', label: "Unpin Response",
onSelect: () => onPinnedResponseId(activeResponse.id), onSelect: () => onPinnedResponseId(activeResponse.id),
leftSlot: <Icon icon="unpin" />, leftSlot: <Icon icon="unpin" />,
hidden: latestResponseId === activeResponse.id, hidden: latestResponseId === activeResponse.id,
disabled: responses.length === 0, disabled: responses.length === 0,
}, },
{ type: 'separator', label: 'History' }, { type: "separator", label: "History" },
{ {
label: `Delete ${responses.length} ${pluralize('Response', responses.length)}`, label: `Delete ${responses.length} ${pluralize("Response", responses.length)}`,
onSelect: deleteAllResponses.mutate, onSelect: deleteAllResponses.mutate,
hidden: responses.length === 0, hidden: responses.length === 0,
disabled: responses.length === 0, disabled: responses.length === 0,
}, },
{ type: 'separator' }, { type: "separator" },
...responses.map((r: HttpResponse) => ({ ...responses.map((r: HttpResponse) => ({
label: ( label: (
<HStack space={2}> <HStack space={2}>
<HttpStatusTag short className="text-xs" response={r} /> <HttpStatusTag short className="text-xs" response={r} />
<span className="text-text-subtle">&rarr;</span>{' '} <span className="text-text-subtle">&rarr;</span>{" "}
<span className="font-mono text-sm">{r.elapsed >= 0 ? `${r.elapsed}ms` : 'n/a'}</span> <span className="font-mono text-sm">{r.elapsed >= 0 ? `${r.elapsed}ms` : "n/a"}</span>
</HStack> </HStack>
), ),
leftSlot: activeResponse?.id === r.id ? <Icon icon="check" /> : <Icon icon="empty" />, leftSlot: activeResponse?.id === r.id ? <Icon icon="check" /> : <Icon icon="empty" />,
@@ -78,7 +78,7 @@ export const RecentHttpResponsesDropdown = function ResponsePane({
> >
<IconButton <IconButton
title="Show response history" title="Show response history"
icon={activeResponse?.id === latestResponseId ? 'history' : 'pin'} icon={activeResponse?.id === latestResponseId ? "history" : "pin"}
className="m-0.5 text-text-subtle" className="m-0.5 text-text-subtle"
size="sm" size="sm"
iconSize="md" iconSize="md"

View File

@@ -1,18 +1,18 @@
import classNames from 'classnames'; import classNames from "classnames";
import { useMemo, useRef } from 'react'; import { useMemo, useRef } from "react";
import { useActiveRequest } from '../hooks/useActiveRequest'; import { useActiveRequest } from "../hooks/useActiveRequest";
import { activeWorkspaceIdAtom } from '../hooks/useActiveWorkspace'; import { activeWorkspaceIdAtom } from "../hooks/useActiveWorkspace";
import { allRequestsAtom } from '../hooks/useAllRequests'; import { allRequestsAtom } from "../hooks/useAllRequests";
import { useHotKey } from '../hooks/useHotKey'; import { useHotKey } from "../hooks/useHotKey";
import { useKeyboardEvent } from '../hooks/useKeyboardEvent'; import { useKeyboardEvent } from "../hooks/useKeyboardEvent";
import { useRecentRequests } from '../hooks/useRecentRequests'; import { useRecentRequests } from "../hooks/useRecentRequests";
import { jotaiStore } from '../lib/jotai'; import { jotaiStore } from "../lib/jotai";
import { resolvedModelName } from '../lib/resolvedModelName'; import { resolvedModelName } from "../lib/resolvedModelName";
import { router } from '../lib/router'; import { router } from "../lib/router";
import { Button } from './core/Button'; import { Button } from "./core/Button";
import type { DropdownItem, DropdownRef } from './core/Dropdown'; import type { DropdownItem, DropdownRef } from "./core/Dropdown";
import { Dropdown } from './core/Dropdown'; import { Dropdown } from "./core/Dropdown";
import { HttpMethodTag } from './core/HttpMethodTag'; import { HttpMethodTag } from "./core/HttpMethodTag";
interface Props { interface Props {
className?: string; className?: string;
@@ -26,13 +26,13 @@ export function RecentRequestsDropdown({ className }: Props) {
// Handle key-up // Handle key-up
// TODO: Somehow make useHotKey have this functionality. Note: e.key does not work // TODO: Somehow make useHotKey have this functionality. Note: e.key does not work
// on Linux, for example, when Control is mapped to CAPS. This will never fire. // on Linux, for example, when Control is mapped to CAPS. This will never fire.
useKeyboardEvent('keyup', 'Control', () => { useKeyboardEvent("keyup", "Control", () => {
if (dropdownRef.current?.isOpen) { if (dropdownRef.current?.isOpen) {
dropdownRef.current?.select?.(); dropdownRef.current?.select?.();
} }
}); });
useHotKey('switcher.prev', () => { useHotKey("switcher.prev", () => {
if (!dropdownRef.current?.isOpen) { if (!dropdownRef.current?.isOpen) {
// Select the second because the first is the current request // Select the second because the first is the current request
dropdownRef.current?.open(1); dropdownRef.current?.open(1);
@@ -41,7 +41,7 @@ export function RecentRequestsDropdown({ className }: Props) {
} }
}); });
useHotKey('switcher.next', () => { useHotKey("switcher.next", () => {
if (!dropdownRef.current?.isOpen) dropdownRef.current?.open(); if (!dropdownRef.current?.isOpen) dropdownRef.current?.open();
dropdownRef.current?.prev?.(); dropdownRef.current?.prev?.();
}); });
@@ -61,7 +61,7 @@ export function RecentRequestsDropdown({ className }: Props) {
leftSlot: <HttpMethodTag short className="text-xs" request={request} />, leftSlot: <HttpMethodTag short className="text-xs" request={request} />,
onSelect: async () => { onSelect: async () => {
await router.navigate({ await router.navigate({
to: '/workspaces/$workspaceId', to: "/workspaces/$workspaceId",
params: { workspaceId: activeWorkspaceId }, params: { workspaceId: activeWorkspaceId },
search: (prev) => ({ ...prev, request_id: request.id }), search: (prev) => ({ ...prev, request_id: request.id }),
}); });
@@ -73,8 +73,8 @@ export function RecentRequestsDropdown({ className }: Props) {
if (recentRequestItems.length === 0) { if (recentRequestItems.length === 0) {
return [ return [
{ {
key: 'no-recent-requests', key: "no-recent-requests",
label: 'No recent requests', label: "No recent requests",
disabled: true, disabled: true,
}, },
]; ];
@@ -90,8 +90,8 @@ export function RecentRequestsDropdown({ className }: Props) {
hotkeyAction="switcher.toggle" hotkeyAction="switcher.toggle"
className={classNames( className={classNames(
className, className,
'truncate pointer-events-auto', "truncate pointer-events-auto",
activeRequest == null && 'text-text-subtlest italic', activeRequest == null && "text-text-subtlest italic",
)} )}
> >
{resolvedModelName(activeRequest)} {resolvedModelName(activeRequest)}

View File

@@ -1,11 +1,11 @@
import type { WebsocketConnection } from '@yaakapp-internal/models'; import type { WebsocketConnection } from "@yaakapp-internal/models";
import { deleteModel, getModel } from '@yaakapp-internal/models'; import { deleteModel, getModel } from "@yaakapp-internal/models";
import { HStack, Icon } from '@yaakapp-internal/ui'; import { HStack, Icon } from "@yaakapp-internal/ui";
import { formatDistanceToNowStrict } from 'date-fns'; import { formatDistanceToNowStrict } from "date-fns";
import { deleteWebsocketConnections } from '../commands/deleteWebsocketConnections'; import { deleteWebsocketConnections } from "../commands/deleteWebsocketConnections";
import { pluralizeCount } from '../lib/pluralize'; import { pluralizeCount } from "../lib/pluralize";
import { Dropdown } from './core/Dropdown'; import { Dropdown } from "./core/Dropdown";
import { IconButton } from './core/IconButton'; import { IconButton } from "./core/IconButton";
interface Props { interface Props {
connections: WebsocketConnection[]; connections: WebsocketConnection[];
@@ -18,20 +18,20 @@ export function RecentWebsocketConnectionsDropdown({
connections, connections,
onPinnedConnectionId, onPinnedConnectionId,
}: Props) { }: Props) {
const latestConnectionId = connections[0]?.id ?? 'n/a'; const latestConnectionId = connections[0]?.id ?? "n/a";
return ( return (
<Dropdown <Dropdown
items={[ items={[
{ {
label: 'Clear Connection', label: "Clear Connection",
onSelect: () => deleteModel(activeConnection), onSelect: () => deleteModel(activeConnection),
disabled: connections.length === 0, disabled: connections.length === 0,
}, },
{ {
label: `Clear ${pluralizeCount('Connection', connections.length)}`, label: `Clear ${pluralizeCount("Connection", connections.length)}`,
onSelect: () => { onSelect: () => {
const request = getModel('websocket_request', activeConnection.requestId); const request = getModel("websocket_request", activeConnection.requestId);
if (request != null) { if (request != null) {
deleteWebsocketConnections.mutate(request); deleteWebsocketConnections.mutate(request);
} }
@@ -39,11 +39,11 @@ export function RecentWebsocketConnectionsDropdown({
hidden: connections.length <= 1, hidden: connections.length <= 1,
disabled: connections.length === 0, disabled: connections.length === 0,
}, },
{ type: 'separator', label: 'History' }, { type: "separator", label: "History" },
...connections.map((c) => ({ ...connections.map((c) => ({
label: ( label: (
<HStack space={2}> <HStack space={2}>
{formatDistanceToNowStrict(`${c.createdAt}Z`)} ago &bull;{' '} {formatDistanceToNowStrict(`${c.createdAt}Z`)} ago &bull;{" "}
<span className="font-mono text-sm">{c.elapsed}ms</span> <span className="font-mono text-sm">{c.elapsed}ms</span>
</HStack> </HStack>
), ),
@@ -54,7 +54,7 @@ export function RecentWebsocketConnectionsDropdown({
> >
<IconButton <IconButton
title="Show connection history" title="Show connection history"
icon={activeConnection?.id === latestConnectionId ? 'history' : 'pin'} icon={activeConnection?.id === latestConnectionId ? "history" : "pin"}
className="m-0.5 text-text-subtle" className="m-0.5 text-text-subtle"
size="sm" size="sm"
iconSize="md" iconSize="md"

View File

@@ -1,11 +1,12 @@
import { workspacesAtom } from '@yaakapp-internal/models'; import { workspacesAtom } from "@yaakapp-internal/models";
import { useAtomValue } from 'jotai'; import { useAtomValue } from "jotai";
import { useEffect } from 'react'; import { useEffect } from "react";
import { getRecentCookieJars } from '../hooks/useRecentCookieJars'; import { getRecentCookieJars } from "../hooks/useRecentCookieJars";
import { getRecentEnvironments } from '../hooks/useRecentEnvironments'; import { getRecentEnvironments } from "../hooks/useRecentEnvironments";
import { getRecentRequests } from '../hooks/useRecentRequests'; import { getRecentRequests } from "../hooks/useRecentRequests";
import { useRecentWorkspaces } from '../hooks/useRecentWorkspaces'; import { useRecentWorkspaces } from "../hooks/useRecentWorkspaces";
import { router } from '../lib/router'; import { fireAndForget } from "../lib/fireAndForget";
import { router } from "../lib/router";
export function RedirectToLatestWorkspace() { export function RedirectToLatestWorkspace() {
const workspaces = useAtomValue(workspacesAtom); const workspaces = useAtomValue(workspacesAtom);
@@ -13,28 +14,30 @@ export function RedirectToLatestWorkspace() {
useEffect(() => { useEffect(() => {
if (workspaces.length === 0 || recentWorkspaces == null) { if (workspaces.length === 0 || recentWorkspaces == null) {
console.log('No workspaces found to redirect to. Skipping.', { console.log("No workspaces found to redirect to. Skipping.", {
workspaces, workspaces,
recentWorkspaces, recentWorkspaces,
}); });
return; return;
} }
(async () => { fireAndForget(
const workspaceId = recentWorkspaces[0] ?? workspaces[0]?.id ?? 'n/a'; (async () => {
const environmentId = (await getRecentEnvironments(workspaceId))[0] ?? null; const workspaceId = recentWorkspaces[0] ?? workspaces[0]?.id ?? "n/a";
const cookieJarId = (await getRecentCookieJars(workspaceId))[0] ?? null; const environmentId = (await getRecentEnvironments(workspaceId))[0] ?? null;
const requestId = (await getRecentRequests(workspaceId))[0] ?? null; const cookieJarId = (await getRecentCookieJars(workspaceId))[0] ?? null;
const params = { workspaceId }; const requestId = (await getRecentRequests(workspaceId))[0] ?? null;
const search = { const params = { workspaceId };
cookie_jar_id: cookieJarId, const search = {
environment_id: environmentId, cookie_jar_id: cookieJarId,
request_id: requestId, environment_id: environmentId,
}; request_id: requestId,
};
console.log('Redirecting to workspace', params, search); console.log("Redirecting to workspace", params, search);
await router.navigate({ to: '/workspaces/$workspaceId', params, search }); await router.navigate({ to: "/workspaces/$workspaceId", params, search });
})(); })(),
);
}, [recentWorkspaces, workspaces, workspaces.length]); }, [recentWorkspaces, workspaces, workspaces.length]);
return null; return null;

View File

@@ -1,20 +1,20 @@
import type { HttpResponse } from '@yaakapp-internal/models'; import type { HttpResponse } from "@yaakapp-internal/models";
import { lazy, Suspense } from 'react'; import { lazy, Suspense } from "react";
import { useHttpRequestBody } from '../hooks/useHttpRequestBody'; import { useHttpRequestBody } from "../hooks/useHttpRequestBody";
import { getMimeTypeFromContentType, languageFromContentType } from '../lib/contentType'; import { getMimeTypeFromContentType, languageFromContentType } from "../lib/contentType";
import { LoadingIcon } from '@yaakapp-internal/ui'; import { LoadingIcon } from "@yaakapp-internal/ui";
import { EmptyStateText } from './EmptyStateText'; import { EmptyStateText } from "./EmptyStateText";
import { AudioViewer } from './responseViewers/AudioViewer'; import { AudioViewer } from "./responseViewers/AudioViewer";
import { CsvViewer } from './responseViewers/CsvViewer'; import { CsvViewer } from "./responseViewers/CsvViewer";
import { ImageViewer } from './responseViewers/ImageViewer'; import { ImageViewer } from "./responseViewers/ImageViewer";
import { MultipartViewer } from './responseViewers/MultipartViewer'; import { MultipartViewer } from "./responseViewers/MultipartViewer";
import { SvgViewer } from './responseViewers/SvgViewer'; import { SvgViewer } from "./responseViewers/SvgViewer";
import { TextViewer } from './responseViewers/TextViewer'; import { TextViewer } from "./responseViewers/TextViewer";
import { VideoViewer } from './responseViewers/VideoViewer'; import { VideoViewer } from "./responseViewers/VideoViewer";
import { WebPageViewer } from './responseViewers/WebPageViewer'; import { WebPageViewer } from "./responseViewers/WebPageViewer";
const PdfViewer = lazy(() => const PdfViewer = lazy(() =>
import('./responseViewers/PdfViewer').then((m) => ({ default: m.PdfViewer })), import("./responseViewers/PdfViewer").then((m) => ({ default: m.PdfViewer })),
); );
interface Props { interface Props {
@@ -48,7 +48,7 @@ function RequestBodyViewerInner({ response }: Props) {
// Try to detect language from content-type header that was sent // Try to detect language from content-type header that was sent
const contentTypeHeader = response.requestHeaders.find( const contentTypeHeader = response.requestHeaders.find(
(h) => h.name.toLowerCase() === 'content-type', (h) => h.name.toLowerCase() === "content-type",
); );
const contentType = contentTypeHeader?.value ?? null; const contentType = contentTypeHeader?.value ?? null;
const mimeType = contentType ? getMimeTypeFromContentType(contentType).essence : null; const mimeType = contentType ? getMimeTypeFromContentType(contentType).essence : null;
@@ -56,7 +56,7 @@ function RequestBodyViewerInner({ response }: Props) {
// Route to appropriate viewer based on content type // Route to appropriate viewer based on content type
if (mimeType?.match(/^multipart/i)) { if (mimeType?.match(/^multipart/i)) {
const boundary = contentType?.split('boundary=')[1] ?? 'unknown'; const boundary = contentType?.split("boundary=")[1] ?? "unknown";
// Create a copy because parseMultipart may detach the buffer // Create a copy because parseMultipart may detach the buffer
const bodyCopy = new Uint8Array(body); const bodyCopy = new Uint8Array(body);
return ( return (

View File

@@ -1,14 +1,14 @@
import type { HttpRequest } from '@yaakapp-internal/models'; import type { HttpRequest } from "@yaakapp-internal/models";
import { patchModel } from '@yaakapp-internal/models'; import { patchModel } from "@yaakapp-internal/models";
import classNames from 'classnames'; import classNames from "classnames";
import { memo, useCallback, useMemo } from 'react'; import { memo, useCallback, useMemo } from "react";
import { showPrompt } from '../lib/prompt'; import { showPrompt } from "../lib/prompt";
import { Button } from './core/Button'; import { Button } from "./core/Button";
import type { DropdownItem } from './core/Dropdown'; import type { DropdownItem } from "./core/Dropdown";
import { HttpMethodTag, HttpMethodTagRaw } from './core/HttpMethodTag'; import { HttpMethodTag, HttpMethodTagRaw } from "./core/HttpMethodTag";
import { Icon } from '@yaakapp-internal/ui'; import { Icon } from "@yaakapp-internal/ui";
import type { RadioDropdownItem } from './core/RadioDropdown'; import type { RadioDropdownItem } from "./core/RadioDropdown";
import { RadioDropdown } from './core/RadioDropdown'; import { RadioDropdown } from "./core/RadioDropdown";
type Props = { type Props = {
request: HttpRequest; request: HttpRequest;
@@ -16,14 +16,14 @@ type Props = {
}; };
const radioItems: RadioDropdownItem<string>[] = [ const radioItems: RadioDropdownItem<string>[] = [
'GET', "GET",
'PUT', "PUT",
'POST', "POST",
'PATCH', "PATCH",
'DELETE', "DELETE",
'OPTIONS', "OPTIONS",
'QUERY', "QUERY",
'HEAD', "HEAD",
].map((m) => ({ ].map((m) => ({
value: m, value: m,
label: <HttpMethodTagRaw method={m} />, label: <HttpMethodTagRaw method={m} />,
@@ -43,17 +43,17 @@ export const RequestMethodDropdown = memo(function RequestMethodDropdown({
const itemsAfter = useMemo<DropdownItem[]>( const itemsAfter = useMemo<DropdownItem[]>(
() => [ () => [
{ {
key: 'custom', key: "custom",
label: 'CUSTOM', label: "CUSTOM",
leftSlot: <Icon icon="sparkles" />, leftSlot: <Icon icon="sparkles" />,
onSelect: async () => { onSelect: async () => {
const newMethod = await showPrompt({ const newMethod = await showPrompt({
id: 'custom-method', id: "custom-method",
label: 'Http Method', label: "Http Method",
title: 'Custom Method', title: "Custom Method",
confirmText: 'Save', confirmText: "Save",
description: 'Enter a custom method name', description: "Enter a custom method name",
placeholder: 'CUSTOM', placeholder: "CUSTOM",
}); });
if (newMethod == null) return; if (newMethod == null) return;
await handleChange(newMethod); await handleChange(newMethod);
@@ -70,7 +70,7 @@ export const RequestMethodDropdown = memo(function RequestMethodDropdown({
itemsAfter={itemsAfter} itemsAfter={itemsAfter}
onChange={handleChange} onChange={handleChange}
> >
<Button size="xs" className={classNames(className, 'text-text-subtle hover:text')}> <Button size="xs" className={classNames(className, "text-text-subtle hover:text")}>
<HttpMethodTag request={request} noAlias /> <HttpMethodTag request={request} noAlias />
</Button> </Button>
</RadioDropdown> </RadioDropdown>

View File

@@ -1,11 +1,11 @@
import type { HttpResponse } from '@yaakapp-internal/models'; import type { HttpResponse } from "@yaakapp-internal/models";
import classNames from 'classnames'; import classNames from "classnames";
import { useMemo } from 'react'; import { useMemo } from "react";
import type { JSX } from 'react/jsx-runtime'; import type { JSX } from "react/jsx-runtime";
import { useHttpResponseEvents } from '../hooks/useHttpResponseEvents'; import { useHttpResponseEvents } from "../hooks/useHttpResponseEvents";
import { CountBadge } from './core/CountBadge'; import { CountBadge } from "./core/CountBadge";
import { DetailsBanner } from './core/DetailsBanner'; import { DetailsBanner } from "./core/DetailsBanner";
import { KeyValueRow, KeyValueRows } from './core/KeyValueRow'; import { KeyValueRow, KeyValueRows } from "./core/KeyValueRow";
interface Props { interface Props {
response: HttpResponse; response: HttpResponse;
@@ -26,37 +26,37 @@ interface ParsedCookie {
function parseCookieHeader(cookieHeader: string): Array<{ name: string; value: string }> { function parseCookieHeader(cookieHeader: string): Array<{ name: string; value: string }> {
// Parse "Cookie: name=value; name2=value2" format // Parse "Cookie: name=value; name2=value2" format
return cookieHeader.split(';').map((pair) => { return cookieHeader.split(";").map((pair) => {
const [name = '', ...valueParts] = pair.split('='); const [name = "", ...valueParts] = pair.split("=");
return { return {
name: name.trim(), name: name.trim(),
value: valueParts.join('=').trim(), value: valueParts.join("=").trim(),
}; };
}); });
} }
function parseSetCookieHeader(setCookieHeader: string): ParsedCookie { function parseSetCookieHeader(setCookieHeader: string): ParsedCookie {
// Parse "Set-Cookie: name=value; Domain=...; Path=..." format // Parse "Set-Cookie: name=value; Domain=...; Path=..." format
const parts = setCookieHeader.split(';').map((p) => p.trim()); const parts = setCookieHeader.split(";").map((p) => p.trim());
const [nameValue = '', ...attributes] = parts; const [nameValue = "", ...attributes] = parts;
const [name = '', ...valueParts] = nameValue.split('='); const [name = "", ...valueParts] = nameValue.split("=");
const cookie: ParsedCookie = { const cookie: ParsedCookie = {
name: name.trim(), name: name.trim(),
value: valueParts.join('=').trim(), value: valueParts.join("=").trim(),
}; };
for (const attr of attributes) { for (const attr of attributes) {
const [key = '', val] = attr.split('=').map((s) => s.trim()); const [key = "", val] = attr.split("=").map((s) => s.trim());
const lowerKey = key.toLowerCase(); const lowerKey = key.toLowerCase();
if (lowerKey === 'domain') cookie.domain = val; if (lowerKey === "domain") cookie.domain = val;
else if (lowerKey === 'path') cookie.path = val; else if (lowerKey === "path") cookie.path = val;
else if (lowerKey === 'expires') cookie.expires = val; else if (lowerKey === "expires") cookie.expires = val;
else if (lowerKey === 'max-age') cookie.maxAge = val; else if (lowerKey === "max-age") cookie.maxAge = val;
else if (lowerKey === 'secure') cookie.secure = true; else if (lowerKey === "secure") cookie.secure = true;
else if (lowerKey === 'httponly') cookie.httpOnly = true; else if (lowerKey === "httponly") cookie.httpOnly = true;
else if (lowerKey === 'samesite') cookie.sameSite = val; else if (lowerKey === "samesite") cookie.sameSite = val;
} }
// Detect if cookie is being deleted // Detect if cookie is being deleted
@@ -94,7 +94,7 @@ export function ResponseCookies({ response }: Props) {
const e = event.event; const e = event.event;
// Cookie headers sent (header_up with name=cookie) // Cookie headers sent (header_up with name=cookie)
if (e.type === 'header_up' && e.name.toLowerCase() === 'cookie') { if (e.type === "header_up" && e.name.toLowerCase() === "cookie") {
const cookies = parseCookieHeader(e.value); const cookies = parseCookieHeader(e.value);
for (const cookie of cookies) { for (const cookie of cookies) {
sentMap.set(cookie.name, cookie); sentMap.set(cookie.name, cookie);
@@ -102,7 +102,7 @@ export function ResponseCookies({ response }: Props) {
} }
// Set-Cookie headers received (header_down with name=set-cookie) // Set-Cookie headers received (header_down with name=set-cookie)
if (e.type === 'header_down' && e.name.toLowerCase() === 'set-cookie') { if (e.type === "header_down" && e.name.toLowerCase() === "set-cookie") {
const cookie = parseSetCookieHeader(e.value); const cookie = parseSetCookieHeader(e.value);
receivedMap.set(cookie.name, cookie); receivedMap.set(cookie.name, cookie);
} }
@@ -130,7 +130,7 @@ export function ResponseCookies({ response }: Props) {
) : ( ) : (
<KeyValueRows> <KeyValueRows>
{sentCookies.map((cookie, i) => ( {sentCookies.map((cookie, i) => (
// biome-ignore lint/suspicious/noArrayIndexKey: none // oxlint-disable-next-line react/no-array-index-key
<KeyValueRow labelColor="primary" key={i} label={cookie.name}> <KeyValueRow labelColor="primary" key={i} label={cookie.name}>
{cookie.value} {cookie.value}
</KeyValueRow> </KeyValueRow>
@@ -153,13 +153,13 @@ export function ResponseCookies({ response }: Props) {
) : ( ) : (
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
{receivedCookies.map((cookie, i) => ( {receivedCookies.map((cookie, i) => (
// biome-ignore lint/suspicious/noArrayIndexKey: none // oxlint-disable-next-line react/no-array-index-key
<div key={i} className="flex flex-col gap-1"> <div key={i} className="flex flex-col gap-1">
<div className="flex items-center gap-2 my-1"> <div className="flex items-center gap-2 my-1">
<span <span
className={classNames( className={classNames(
'font-mono text-editor select-auto cursor-auto', "font-mono text-editor select-auto cursor-auto",
cookie.isDeleted ? 'line-through opacity-60 text-text-subtle' : 'text-text', cookie.isDeleted ? "line-through opacity-60 text-text-subtle" : "text-text",
)} )}
> >
{cookie.name} {cookie.name}

View File

@@ -1,10 +1,10 @@
import { openUrl } from '@tauri-apps/plugin-opener'; import { openUrl } from "@tauri-apps/plugin-opener";
import type { HttpResponse } from '@yaakapp-internal/models'; import type { HttpResponse } from "@yaakapp-internal/models";
import { useMemo } from 'react'; import { useMemo } from "react";
import { CountBadge } from './core/CountBadge'; import { CountBadge } from "./core/CountBadge";
import { DetailsBanner } from './core/DetailsBanner'; import { DetailsBanner } from "./core/DetailsBanner";
import { IconButton } from './core/IconButton'; import { IconButton } from "./core/IconButton";
import { KeyValueRow, KeyValueRows } from './core/KeyValueRow'; import { KeyValueRow, KeyValueRows } from "./core/KeyValueRow";
interface Props { interface Props {
response: HttpResponse; response: HttpResponse;
@@ -62,7 +62,7 @@ export function ResponseHeaders({ response }: Props) {
) : ( ) : (
<KeyValueRows> <KeyValueRows>
{requestHeaders.map((h, i) => ( {requestHeaders.map((h, i) => (
// biome-ignore lint/suspicious/noArrayIndexKey: none // oxlint-disable-next-line react/no-array-index-key
<KeyValueRow labelColor="primary" key={i} label={h.name}> <KeyValueRow labelColor="primary" key={i} label={h.name}>
{h.value} {h.value}
</KeyValueRow> </KeyValueRow>
@@ -84,7 +84,7 @@ export function ResponseHeaders({ response }: Props) {
) : ( ) : (
<KeyValueRows> <KeyValueRows>
{responseHeaders.map((h, i) => ( {responseHeaders.map((h, i) => (
// biome-ignore lint/suspicious/noArrayIndexKey: none // oxlint-disable-next-line react/no-array-index-key
<KeyValueRow labelColor="info" key={i} label={h.name}> <KeyValueRow labelColor="info" key={i} label={h.name}>
{h.value} {h.value}
</KeyValueRow> </KeyValueRow>

View File

@@ -1,7 +1,7 @@
import { openUrl } from '@tauri-apps/plugin-opener'; import { openUrl } from "@tauri-apps/plugin-opener";
import type { HttpResponse } from '@yaakapp-internal/models'; import type { HttpResponse } from "@yaakapp-internal/models";
import { IconButton } from './core/IconButton'; import { IconButton } from "./core/IconButton";
import { KeyValueRow, KeyValueRows } from './core/KeyValueRow'; import { KeyValueRow, KeyValueRows } from "./core/KeyValueRow";
interface Props { interface Props {
response: HttpResponse; response: HttpResponse;

View File

@@ -1,13 +1,13 @@
import { Button, FormattedError, Heading, VStack } from '@yaakapp-internal/ui'; import { Button, FormattedError, Heading, VStack } from "@yaakapp-internal/ui";
import { DetailsBanner } from './core/DetailsBanner'; import { DetailsBanner } from "./core/DetailsBanner";
export default function RouteError({ error }: { error: unknown }) { export default function RouteError({ error }: { error: unknown }) {
console.log('Error', error); console.log("Error", error);
const stringified = JSON.stringify(error); const stringified = JSON.stringify(error);
// biome-ignore lint/suspicious/noExplicitAny: none // biome-ignore lint/suspicious/noExplicitAny: none
const message = (error as any).message ?? stringified; const message = (error as any).message ?? stringified;
const stack = const stack =
typeof error === 'object' && error != null && 'stack' in error ? String(error.stack) : null; typeof error === "object" && error != null && "stack" in error ? String(error.stack) : null;
return ( return (
<div className="flex items-center justify-center h-full"> <div className="flex items-center justify-center h-full">
<VStack space={5} className="w-[50rem] !h-auto"> <VStack space={5} className="w-[50rem] !h-auto">
@@ -28,7 +28,7 @@ export default function RouteError({ error }: { error: unknown }) {
<Button <Button
color="primary" color="primary"
onClick={async () => { onClick={async () => {
window.location.assign('/'); window.location.assign("/");
}} }}
> >
Go Home Go Home

View File

@@ -1,17 +1,17 @@
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'; import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
import { open } from '@tauri-apps/plugin-dialog'; import { open } from "@tauri-apps/plugin-dialog";
import { HStack } from '@yaakapp-internal/ui'; import { HStack } from "@yaakapp-internal/ui";
import classNames from 'classnames'; import classNames from "classnames";
import mime from 'mime'; import mime from "mime";
import type { ReactNode } from 'react'; import type { ReactNode } from "react";
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from "react";
import type { ButtonProps } from './core/Button'; import type { ButtonProps } from "./core/Button";
import { Button } from './core/Button'; import { Button } from "./core/Button";
import { IconButton } from './core/IconButton'; import { IconButton } from "./core/IconButton";
import { IconTooltip } from './core/IconTooltip'; import { IconTooltip } from "./core/IconTooltip";
import { Label } from './core/Label'; import { Label } from "./core/Label";
type Props = Omit<ButtonProps, 'type'> & { type Props = Omit<ButtonProps, "type"> & {
onChange: (value: { filePath: string | null; contentType: string | null }) => void; onChange: (value: { filePath: string | null; contentType: string | null }) => void;
filePath: string | null; filePath: string | null;
nameOverride?: string | null; nameOverride?: string | null;
@@ -33,14 +33,14 @@ export function SelectFile({
directory, directory,
noun, noun,
nameOverride, nameOverride,
size = 'sm', size = "sm",
label, label,
help, help,
...props ...props
}: Props) { }: Props) {
const handleClick = async () => { const handleClick = async () => {
const filePath = await open({ const filePath = await open({
title: directory ? 'Select Folder' : 'Select File', title: directory ? "Select Folder" : "Select File",
multiple: false, multiple: false,
directory, directory,
}); });
@@ -53,8 +53,8 @@ export function SelectFile({
onChange({ filePath: null, contentType: null }); onChange({ filePath: null, contentType: null });
}; };
const itemLabel = noun ?? (directory ? 'Folder' : 'File'); const itemLabel = noun ?? (directory ? "Folder" : "File");
const selectOrChange = (filePath ? 'Change ' : 'Select ') + itemLabel; const selectOrChange = (filePath ? "Change " : "Select ") + itemLabel;
const [isHovering, setIsHovering] = useState(false); const [isHovering, setIsHovering] = useState(false);
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
@@ -66,20 +66,20 @@ export function SelectFile({
const setup = async () => { const setup = async () => {
const webview = getCurrentWebviewWindow(); const webview = getCurrentWebviewWindow();
unlisten = await webview.onDragDropEvent((event) => { unlisten = await webview.onDragDropEvent((event) => {
if (event.payload.type === 'over') { if (event.payload.type === "over") {
const p = event.payload.position; const p = event.payload.position;
const r = ref.current?.getBoundingClientRect(); const r = ref.current?.getBoundingClientRect();
if (r == null) return; if (r == null) return;
const isOver = p.x >= r.left && p.x <= r.right && p.y >= r.top && p.y <= r.bottom; const isOver = p.x >= r.left && p.x <= r.right && p.y >= r.top && p.y <= r.bottom;
console.log('IS OVER', isOver); console.log("IS OVER", isOver);
setIsHovering(isOver); setIsHovering(isOver);
} else if (event.payload.type === 'drop' && isHovering) { } else if (event.payload.type === "drop" && isHovering) {
console.log('User dropped', event.payload.paths); console.log("User dropped", event.payload.paths);
const p = event.payload.paths[0]; const p = event.payload.paths[0];
if (p) onChange({ filePath: p, contentType: null }); if (p) onChange({ filePath: p, contentType: null });
setIsHovering(false); setIsHovering(false);
} else { } else {
console.log('File drop cancelled'); console.log("File drop cancelled");
setIsHovering(false); setIsHovering(false);
} }
}); });
@@ -103,12 +103,12 @@ export function SelectFile({
<Button <Button
className={classNames( className={classNames(
className, className,
'rtl mr-1.5', "rtl mr-1.5",
inline && 'w-full', inline && "w-full",
filePath && inline && 'font-mono text-xs', filePath && inline && "font-mono text-xs",
isHovering && '!border-notice', isHovering && "!border-notice",
)} )}
color={isHovering ? 'primary' : 'secondary'} color={isHovering ? "primary" : "secondary"}
onClick={handleClick} onClick={handleClick}
size={size} size={size}
{...props} {...props}
@@ -121,7 +121,7 @@ export function SelectFile({
<> <>
{filePath && ( {filePath && (
<IconButton <IconButton
size={size === 'auto' ? 'md' : size} size={size === "auto" ? "md" : size}
variant="border" variant="border"
icon="x" icon="x"
title={`Unset ${itemLabel}`} title={`Unset ${itemLabel}`}
@@ -130,10 +130,10 @@ export function SelectFile({
)} )}
<div <div
className={classNames( className={classNames(
'truncate rtl pl-1.5 pr-3 text-text', "truncate rtl pl-1.5 pr-3 text-text",
filePath && 'font-mono', filePath && "font-mono",
size === 'xs' && filePath && 'text-xs', size === "xs" && filePath && "text-xs",
size === 'sm' && filePath && 'text-sm', size === "sm" && filePath && "text-sm",
)} )}
> >
{rtlEscapeChar} {rtlEscapeChar}

View File

@@ -1,37 +1,37 @@
import { useSearch } from '@tanstack/react-router'; import { useSearch } from "@tanstack/react-router";
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'; import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
import { type } from '@tauri-apps/plugin-os'; import { type } from "@tauri-apps/plugin-os";
import { useLicense } from '@yaakapp-internal/license'; import { useLicense } from "@yaakapp-internal/license";
import { pluginsAtom, settingsAtom } from '@yaakapp-internal/models'; import { pluginsAtom, settingsAtom } from "@yaakapp-internal/models";
import { HeaderSize, HStack, Icon } from '@yaakapp-internal/ui'; import { HeaderSize, HStack, Icon } from "@yaakapp-internal/ui";
import classNames from 'classnames'; import classNames from "classnames";
import { useAtomValue } from 'jotai'; import { useAtomValue } from "jotai";
import { useKeyPressEvent } from 'react-use'; import { useKeyPressEvent } from "react-use";
import { appInfo } from '../../lib/appInfo'; import { appInfo } from "../../lib/appInfo";
import { capitalize } from '../../lib/capitalize'; import { capitalize } from "../../lib/capitalize";
import { CountBadge } from '../core/CountBadge'; import { CountBadge } from "../core/CountBadge";
import { TabContent, type TabItem, Tabs } from '../core/Tabs/Tabs'; import { TabContent, type TabItem, Tabs } from "../core/Tabs/Tabs";
import { SettingsCertificates } from './SettingsCertificates'; import { SettingsCertificates } from "./SettingsCertificates";
import { SettingsGeneral } from './SettingsGeneral'; import { SettingsGeneral } from "./SettingsGeneral";
import { SettingsHotkeys } from './SettingsHotkeys'; import { SettingsHotkeys } from "./SettingsHotkeys";
import { SettingsInterface } from './SettingsInterface'; import { SettingsInterface } from "./SettingsInterface";
import { SettingsLicense } from './SettingsLicense'; import { SettingsLicense } from "./SettingsLicense";
import { SettingsPlugins } from './SettingsPlugins'; import { SettingsPlugins } from "./SettingsPlugins";
import { SettingsProxy } from './SettingsProxy'; import { SettingsProxy } from "./SettingsProxy";
import { SettingsTheme } from './SettingsTheme'; import { SettingsTheme } from "./SettingsTheme";
interface Props { interface Props {
hide?: () => void; hide?: () => void;
} }
const TAB_GENERAL = 'general'; const TAB_GENERAL = "general";
const TAB_INTERFACE = 'interface'; const TAB_INTERFACE = "interface";
const TAB_THEME = 'theme'; const TAB_THEME = "theme";
const TAB_SHORTCUTS = 'shortcuts'; const TAB_SHORTCUTS = "shortcuts";
const TAB_PROXY = 'proxy'; const TAB_PROXY = "proxy";
const TAB_CERTIFICATES = 'certificates'; const TAB_CERTIFICATES = "certificates";
const TAB_PLUGINS = 'plugins'; const TAB_PLUGINS = "plugins";
const TAB_LICENSE = 'license'; const TAB_LICENSE = "license";
const tabs = [ const tabs = [
TAB_GENERAL, TAB_GENERAL,
TAB_THEME, TAB_THEME,
@@ -45,16 +45,16 @@ const tabs = [
export type SettingsTab = (typeof tabs)[number]; export type SettingsTab = (typeof tabs)[number];
export default function Settings({ hide }: Props) { export default function Settings({ hide }: Props) {
const { tab: tabFromQuery } = useSearch({ from: '/workspaces/$workspaceId/settings' }); const { tab: tabFromQuery } = useSearch({ from: "/workspaces/$workspaceId/settings" });
// Parse tab and subtab (e.g., "plugins:installed") // Parse tab and subtab (e.g., "plugins:installed")
const [mainTab, subtab] = tabFromQuery?.split(':') ?? []; const [mainTab, subtab] = tabFromQuery?.split(":") ?? [];
const settings = useAtomValue(settingsAtom); const settings = useAtomValue(settingsAtom);
const plugins = useAtomValue(pluginsAtom); const plugins = useAtomValue(pluginsAtom);
const licenseCheck = useLicense(); const licenseCheck = useLicense();
// Close settings window on escape // Close settings window on escape
// TODO: Could this be put in a better place? Eg. in Rust key listener when creating the window // TODO: Could this be put in a better place? Eg. in Rust key listener when creating the window
useKeyPressEvent('Escape', async () => { useKeyPressEvent("Escape", async () => {
if (hide != null) { if (hide != null) {
// It's being shown in a dialog, so close the dialog // It's being shown in a dialog, so close the dialog
hide(); hide();
@@ -65,7 +65,7 @@ export default function Settings({ hide }: Props) {
}); });
return ( return (
<div className={classNames('grid grid-rows-[auto_minmax(0,1fr)] h-full')}> <div className={classNames("grid grid-rows-[auto_minmax(0,1fr)] h-full")}>
{hide ? ( {hide ? (
<span /> <span />
) : ( ) : (
@@ -85,7 +85,7 @@ export default function Settings({ hide }: Props) {
justifyContent="center" justifyContent="center"
className="w-full h-full grid grid-cols-[1fr_auto] pointer-events-none" className="w-full h-full grid grid-cols-[1fr_auto] pointer-events-none"
> >
<div className={classNames(type() === 'macos' ? 'text-center' : 'pl-2')}>Settings</div> <div className={classNames(type() === "macos" ? "text-center" : "pl-2")}>Settings</div>
</HStack> </HStack>
</HeaderSize> </HeaderSize>
)} )}
@@ -122,10 +122,10 @@ export default function Settings({ hide }: Props) {
value === TAB_CERTIFICATES ? ( value === TAB_CERTIFICATES ? (
<CountBadge count={settings.clientCertificates.length} /> <CountBadge count={settings.clientCertificates.length} />
) : value === TAB_PLUGINS ? ( ) : value === TAB_PLUGINS ? (
<CountBadge count={plugins.filter((p) => p.source !== 'bundled').length} /> <CountBadge count={plugins.filter((p) => p.source !== "bundled").length} />
) : value === TAB_PROXY && settings.proxy?.type === 'enabled' ? ( ) : value === TAB_PROXY && settings.proxy?.type === "enabled" ? (
<CountBadge count /> <CountBadge count />
) : value === TAB_LICENSE && licenseCheck.check.data?.status === 'personal_use' ? ( ) : value === TAB_LICENSE && licenseCheck.check.data?.status === "personal_use" ? (
<CountBadge count color="notice" /> <CountBadge count color="notice" />
) : null, ) : null,
}), }),

View File

@@ -1,20 +1,20 @@
import type { ClientCertificate } from '@yaakapp-internal/models'; import type { ClientCertificate } from "@yaakapp-internal/models";
import { patchModel, settingsAtom } from '@yaakapp-internal/models'; import { patchModel, settingsAtom } from "@yaakapp-internal/models";
import { Heading, HStack, InlineCode, VStack } from '@yaakapp-internal/ui'; import { Heading, HStack, InlineCode, VStack } from "@yaakapp-internal/ui";
import { useAtomValue } from 'jotai'; import { useAtomValue } from "jotai";
import { useRef } from 'react'; import { useRef } from "react";
import { showConfirmDelete } from '../../lib/confirm'; import { showConfirmDelete } from "../../lib/confirm";
import { Button } from '../core/Button'; import { Button } from "../core/Button";
import { Checkbox } from '../core/Checkbox'; import { Checkbox } from "../core/Checkbox";
import { DetailsBanner } from '../core/DetailsBanner'; import { DetailsBanner } from "../core/DetailsBanner";
import { IconButton } from '../core/IconButton'; import { IconButton } from "../core/IconButton";
import { PlainInput } from '../core/PlainInput'; import { PlainInput } from "../core/PlainInput";
import { Separator } from '../core/Separator'; import { Separator } from "../core/Separator";
import { SelectFile } from '../SelectFile'; import { SelectFile } from "../SelectFile";
function createEmptyCertificate(): ClientCertificate { function createEmptyCertificate(): ClientCertificate {
return { return {
host: '', host: "",
port: null, port: null,
crtFile: null, crtFile: null,
keyFile: null, keyFile: null,
@@ -42,11 +42,11 @@ function CertificateEditor({ certificate, index, onUpdate, onRemove }: Certifica
const hasPfx = Boolean(certificate.pfxFile && certificate.pfxFile.length > 0); const hasPfx = Boolean(certificate.pfxFile && certificate.pfxFile.length > 0);
const hasCrtKey = Boolean( const hasCrtKey = Boolean(
(certificate.crtFile && certificate.crtFile.length > 0) || (certificate.crtFile && certificate.crtFile.length > 0) ||
(certificate.keyFile && certificate.keyFile.length > 0), (certificate.keyFile && certificate.keyFile.length > 0),
); );
// Determine certificate type for display // Determine certificate type for display
const certType = hasPfx ? 'PFX' : hasCrtKey ? 'CERT' : null; const certType = hasPfx ? "PFX" : hasCrtKey ? "CERT" : null;
const defaultOpen = useRef<boolean>(!certificate.host); const defaultOpen = useRef<boolean>(!certificate.host);
return ( return (
@@ -58,9 +58,9 @@ function CertificateEditor({ certificate, index, onUpdate, onRemove }: Certifica
<Checkbox <Checkbox
className="ml-1" className="ml-1"
checked={certificate.enabled ?? true} checked={certificate.enabled ?? true}
title={certificate.enabled ? 'Disable certificate' : 'Enable certificate'} title={certificate.enabled ? "Disable certificate" : "Enable certificate"}
hideLabel hideLabel
onChange={(enabled) => updateField('enabled', enabled)} onChange={(enabled) => updateField("enabled", enabled)}
/> />
{certificate.host ? ( {certificate.host ? (
@@ -101,7 +101,7 @@ function CertificateEditor({ certificate, index, onUpdate, onRemove }: Certifica
size="sm" size="sm"
required required
defaultValue={certificate.host} defaultValue={certificate.host}
onChange={(host) => updateField('host', host)} onChange={(host) => updateField("host", host)}
/> />
<PlainInput <PlainInput
label="Port" label="Port"
@@ -119,8 +119,8 @@ function CertificateEditor({ certificate, index, onUpdate, onRemove }: Certifica
} }
size="sm" size="sm"
className="w-24" className="w-24"
defaultValue={certificate.port?.toString() ?? ''} defaultValue={certificate.port?.toString() ?? ""}
onChange={(port) => updateField('port', port ? parseInt(port, 10) : null)} onChange={(port) => updateField("port", port ? parseInt(port, 10) : null)}
/> />
</HStack> </HStack>
@@ -133,7 +133,7 @@ function CertificateEditor({ certificate, index, onUpdate, onRemove }: Certifica
filePath={certificate.crtFile ?? null} filePath={certificate.crtFile ?? null}
size="sm" size="sm"
disabled={hasPfx} disabled={hasPfx}
onChange={({ filePath }) => updateField('crtFile', filePath)} onChange={({ filePath }) => updateField("crtFile", filePath)}
/> />
<SelectFile <SelectFile
label="KEY File" label="KEY File"
@@ -141,7 +141,7 @@ function CertificateEditor({ certificate, index, onUpdate, onRemove }: Certifica
filePath={certificate.keyFile ?? null} filePath={certificate.keyFile ?? null}
size="sm" size="sm"
disabled={hasPfx} disabled={hasPfx}
onChange={({ filePath }) => updateField('keyFile', filePath)} onChange={({ filePath }) => updateField("keyFile", filePath)}
/> />
</VStack> </VStack>
@@ -153,15 +153,15 @@ function CertificateEditor({ certificate, index, onUpdate, onRemove }: Certifica
filePath={certificate.pfxFile ?? null} filePath={certificate.pfxFile ?? null}
size="sm" size="sm"
disabled={hasCrtKey} disabled={hasCrtKey}
onChange={({ filePath }) => updateField('pfxFile', filePath)} onChange={({ filePath }) => updateField("pfxFile", filePath)}
/> />
<PlainInput <PlainInput
label="Passphrase" label="Passphrase"
size="sm" size="sm"
type="password" type="password"
defaultValue={certificate.passphrase ?? ''} defaultValue={certificate.passphrase ?? ""}
onChange={(passphrase) => updateField('passphrase', passphrase || null)} onChange={(passphrase) => updateField("passphrase", passphrase || null)}
/> />
</VStack> </VStack>
</DetailsBanner> </DetailsBanner>
@@ -191,15 +191,15 @@ export function SettingsCertificates() {
const cert = certificates[index]; const cert = certificates[index];
if (cert == null) return; if (cert == null) return;
const host = cert.host || 'this certificate'; const host = cert.host || "this certificate";
const port = cert.port != null ? `:${cert.port}` : ''; const port = cert.port != null ? `:${cert.port}` : "";
const confirmed = await showConfirmDelete({ const confirmed = await showConfirmDelete({
id: 'confirm-remove-certificate', id: "confirm-remove-certificate",
title: 'Delete Certificate', title: "Delete Certificate",
description: ( description: (
<> <>
Permanently delete certificate for{' '} Permanently delete certificate for{" "}
<InlineCode> <InlineCode>
{host} {host}
{port} {port}

View File

@@ -1,18 +1,18 @@
import { revealItemInDir } from '@tauri-apps/plugin-opener'; import { revealItemInDir } from "@tauri-apps/plugin-opener";
import { patchModel, settingsAtom } from '@yaakapp-internal/models'; import { patchModel, settingsAtom } from "@yaakapp-internal/models";
import { Heading, VStack } from '@yaakapp-internal/ui'; import { Heading, VStack } from "@yaakapp-internal/ui";
import { useAtomValue } from 'jotai'; import { useAtomValue } from "jotai";
import { activeWorkspaceAtom } from '../../hooks/useActiveWorkspace'; import { activeWorkspaceAtom } from "../../hooks/useActiveWorkspace";
import { useCheckForUpdates } from '../../hooks/useCheckForUpdates'; import { useCheckForUpdates } from "../../hooks/useCheckForUpdates";
import { appInfo } from '../../lib/appInfo'; import { appInfo } from "../../lib/appInfo";
import { revealInFinderText } from '../../lib/reveal'; import { revealInFinderText } from "../../lib/reveal";
import { CargoFeature } from '../CargoFeature'; import { CargoFeature } from "../CargoFeature";
import { Checkbox } from '../core/Checkbox'; import { Checkbox } from "../core/Checkbox";
import { IconButton } from '../core/IconButton'; import { IconButton } from "../core/IconButton";
import { KeyValueRow, KeyValueRows } from '../core/KeyValueRow'; import { KeyValueRow, KeyValueRows } from "../core/KeyValueRow";
import { PlainInput } from '../core/PlainInput'; import { PlainInput } from "../core/PlainInput";
import { Select } from '../core/Select'; import { Select } from "../core/Select";
import { Separator } from '../core/Separator'; import { Separator } from "../core/Separator";
export function SettingsGeneral() { export function SettingsGeneral() {
const workspace = useAtomValue(activeWorkspaceAtom); const workspace = useAtomValue(activeWorkspaceAtom);
@@ -40,8 +40,8 @@ export function SettingsGeneral() {
value={settings.updateChannel} value={settings.updateChannel}
onChange={(updateChannel) => patchModel(settings, { updateChannel })} onChange={(updateChannel) => patchModel(settings, { updateChannel })}
options={[ options={[
{ label: 'Stable', value: 'stable' }, { label: "Stable", value: "stable" },
{ label: 'Beta (more frequent)', value: 'beta' }, { label: "Beta (more frequent)", value: "beta" },
]} ]}
/> />
<IconButton <IconButton
@@ -56,15 +56,15 @@ export function SettingsGeneral() {
<Select <Select
name="autoupdate" name="autoupdate"
value={settings.autoupdate ? 'auto' : 'manual'} value={settings.autoupdate ? "auto" : "manual"}
label="Update Behavior" label="Update Behavior"
labelPosition="left" labelPosition="left"
size="sm" size="sm"
labelClassName="w-[14rem]" labelClassName="w-[14rem]"
onChange={(v) => patchModel(settings, { autoupdate: v === 'auto' })} onChange={(v) => patchModel(settings, { autoupdate: v === "auto" })}
options={[ options={[
{ label: 'Automatic', value: 'auto' }, { label: "Automatic", value: "auto" },
{ label: 'Manual', value: 'manual' }, { label: "Manual", value: "manual" },
]} ]}
/> />
<Checkbox <Checkbox
@@ -96,7 +96,7 @@ export function SettingsGeneral() {
<Separator className="my-4" /> <Separator className="my-4" />
<Heading level={2}> <Heading level={2}>
Workspace{' '} Workspace{" "}
<div className="inline-block ml-1 bg-surface-highlight px-2 py-0.5 rounded text text-shrink"> <div className="inline-block ml-1 bg-surface-highlight px-2 py-0.5 rounded text text-shrink">
{workspace.name} {workspace.name}
</div> </div>

View File

@@ -1,4 +1,4 @@
import { patchModel, settingsAtom } from '@yaakapp-internal/models'; import { patchModel, settingsAtom } from "@yaakapp-internal/models";
import { import {
Heading, Heading,
HStack, HStack,
@@ -10,11 +10,11 @@ import {
TableHeaderCell, TableHeaderCell,
TableRow, TableRow,
VStack, VStack,
} from '@yaakapp-internal/ui'; } from "@yaakapp-internal/ui";
import classNames from 'classnames'; import classNames from "classnames";
import { fuzzyMatch } from 'fuzzbunny'; import { fuzzyMatch } from "fuzzbunny";
import { useAtomValue } from 'jotai'; import { useAtomValue } from "jotai";
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from "react";
import { import {
defaultHotkeys, defaultHotkeys,
formatHotkeyString, formatHotkeyString,
@@ -23,23 +23,23 @@ import {
hotkeyActions, hotkeyActions,
hotkeysAtom, hotkeysAtom,
useHotkeyLabel, useHotkeyLabel,
} from '../../hooks/useHotKey'; } from "../../hooks/useHotKey";
import { capitalize } from '../../lib/capitalize'; import { capitalize } from "../../lib/capitalize";
import { showDialog } from '../../lib/dialog'; import { showDialog } from "../../lib/dialog";
import { Button } from '../core/Button'; import { Button } from "../core/Button";
import { Dropdown, type DropdownItem } from '../core/Dropdown'; import { Dropdown, type DropdownItem } from "../core/Dropdown";
import { HotkeyRaw } from '../core/Hotkey'; import { HotkeyRaw } from "../core/Hotkey";
import { IconButton } from '../core/IconButton'; import { IconButton } from "../core/IconButton";
import { PlainInput } from '../core/PlainInput'; import { PlainInput } from "../core/PlainInput";
const HOLD_KEYS = ['Shift', 'Control', 'Alt', 'Meta']; const HOLD_KEYS = ["Shift", "Control", "Alt", "Meta"];
const LAYOUT_INSENSITIVE_KEYS = [ const LAYOUT_INSENSITIVE_KEYS = [
'Equal', "Equal",
'Minus', "Minus",
'BracketLeft', "BracketLeft",
'BracketRight', "BracketRight",
'Backquote', "Backquote",
'Space', "Space",
]; ];
/** Convert a KeyboardEvent to a hotkey string like "Meta+Shift+k" or "Control+Shift+k" */ /** Convert a KeyboardEvent to a hotkey string like "Meta+Shift+k" or "Control+Shift+k" */
@@ -53,37 +53,37 @@ function eventToHotkeyString(e: KeyboardEvent): string | null {
// Add modifiers in consistent order (Meta, Control, Alt, Shift) // Add modifiers in consistent order (Meta, Control, Alt, Shift)
if (e.metaKey) { if (e.metaKey) {
parts.push('Meta'); parts.push("Meta");
} }
if (e.ctrlKey) { if (e.ctrlKey) {
parts.push('Control'); parts.push("Control");
} }
if (e.altKey) { if (e.altKey) {
parts.push('Alt'); parts.push("Alt");
} }
if (e.shiftKey) { if (e.shiftKey) {
parts.push('Shift'); parts.push("Shift");
} }
// Get the main key - use the same logic as useHotKey.ts // Get the main key - use the same logic as useHotKey.ts
const key = LAYOUT_INSENSITIVE_KEYS.includes(e.code) ? e.code : e.key; const key = LAYOUT_INSENSITIVE_KEYS.includes(e.code) ? e.code : e.key;
parts.push(key); parts.push(key);
return parts.join('+'); return parts.join("+");
} }
export function SettingsHotkeys() { export function SettingsHotkeys() {
const settings = useAtomValue(settingsAtom); const settings = useAtomValue(settingsAtom);
const hotkeys = useAtomValue(hotkeysAtom); const hotkeys = useAtomValue(hotkeysAtom);
const [filter, setFilter] = useState(''); const [filter, setFilter] = useState("");
const filteredActions = useMemo(() => { const filteredActions = useMemo(() => {
if (!filter.trim()) { if (!filter.trim()) {
return hotkeyActions; return hotkeyActions;
} }
return hotkeyActions.filter((action) => { return hotkeyActions.filter((action) => {
const scope = getHotkeyScope(action).replace(/_/g, ' '); const scope = getHotkeyScope(action).replace(/_/g, " ");
const label = action.replace(/[_.]/g, ' '); const label = action.replace(/[_.]/g, " ");
const searchText = `${scope} ${label}`; const searchText = `${scope} ${label}`;
return fuzzyMatch(searchText, filter) != null; return fuzzyMatch(searchText, filter) != null;
}); });
@@ -160,7 +160,7 @@ interface HotkeyRowProps {
function HotkeyRow({ action, currentKeys, defaultKeys, onSave, onReset }: HotkeyRowProps) { function HotkeyRow({ action, currentKeys, defaultKeys, onSave, onReset }: HotkeyRowProps) {
const label = useHotkeyLabel(action); const label = useHotkeyLabel(action);
const scope = capitalize(getHotkeyScope(action).replace(/_/g, ' ')); const scope = capitalize(getHotkeyScope(action).replace(/_/g, " "));
const isCustomized = !arraysEqual(currentKeys, defaultKeys); const isCustomized = !arraysEqual(currentKeys, defaultKeys);
const isDisabled = currentKeys.length === 0; const isDisabled = currentKeys.length === 0;
@@ -168,7 +168,7 @@ function HotkeyRow({ action, currentKeys, defaultKeys, onSave, onReset }: Hotkey
showDialog({ showDialog({
id: `record-hotkey-${action}`, id: `record-hotkey-${action}`,
title: label, title: label,
size: 'sm', size: "sm",
render: ({ hide }) => ( render: ({ hide }) => (
<RecordHotkeyDialog <RecordHotkeyDialog
label={label} label={label}
@@ -197,7 +197,7 @@ function HotkeyRow({ action, currentKeys, defaultKeys, onSave, onReset }: Hotkey
// Build dropdown items dynamically // Build dropdown items dynamically
const dropdownItems: DropdownItem[] = [ const dropdownItems: DropdownItem[] = [
{ {
label: 'Add Keyboard Shortcut', label: "Add Keyboard Shortcut",
leftSlot: <Icon icon="plus" />, leftSlot: <Icon icon="plus" />,
onSelect: handleStartRecording, onSelect: handleStartRecording,
}, },
@@ -221,10 +221,10 @@ function HotkeyRow({ action, currentKeys, defaultKeys, onSave, onReset }: Hotkey
if (currentKeys.length > 1) { if (currentKeys.length > 1) {
dropdownItems.push( dropdownItems.push(
{ {
type: 'separator', type: "separator",
}, },
{ {
label: 'Remove All Shortcuts', label: "Remove All Shortcuts",
leftSlot: <Icon icon="trash" />, leftSlot: <Icon icon="trash" />,
onSelect: handleClearAll, onSelect: handleClearAll,
}, },
@@ -234,10 +234,10 @@ function HotkeyRow({ action, currentKeys, defaultKeys, onSave, onReset }: Hotkey
if (isCustomized) { if (isCustomized) {
dropdownItems.push({ dropdownItems.push({
type: 'separator', type: "separator",
}); });
dropdownItems.push({ dropdownItems.push({
label: 'Reset to Default', label: "Reset to Default",
leftSlot: <Icon icon="refresh" />, leftSlot: <Icon icon="refresh" />,
onSelect: onReset, onSelect: onReset,
}); });
@@ -300,7 +300,7 @@ function RecordHotkeyDialog({ label, onSave, onCancel }: RecordHotkeyDialogProps
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
if (e.key === 'Escape') { if (e.key === "Escape") {
onCancel(); onCancel();
return; return;
} }
@@ -311,9 +311,9 @@ function RecordHotkeyDialog({ label, onSave, onCancel }: RecordHotkeyDialogProps
} }
}; };
window.addEventListener('keydown', handleKeyDown, { capture: true }); window.addEventListener("keydown", handleKeyDown, { capture: true });
return () => { return () => {
window.removeEventListener('keydown', handleKeyDown, { capture: true }); window.removeEventListener("keydown", handleKeyDown, { capture: true });
}; };
}, [isFocused, onCancel]); }, [isFocused, onCancel]);
@@ -340,9 +340,9 @@ function RecordHotkeyDialog({ label, onSave, onCancel }: RecordHotkeyDialogProps
e.currentTarget.focus(); e.currentTarget.focus();
}} }}
className={classNames( className={classNames(
'flex items-center justify-center', "flex items-center justify-center",
'px-4 py-2 rounded-lg bg-surface-highlight border outline-none cursor-default w-full', "px-4 py-2 rounded-lg bg-surface-highlight border outline-none cursor-default w-full",
'border-border-subtle focus:border-border-focus', "border-border-subtle focus:border-border-focus",
)} )}
> >
{recordedKey ? ( {recordedKey ? (

View File

@@ -1,31 +1,31 @@
import { type } from '@tauri-apps/plugin-os'; import { type } from "@tauri-apps/plugin-os";
import { useFonts } from '@yaakapp-internal/fonts'; import { useFonts } from "@yaakapp-internal/fonts";
import { useLicense } from '@yaakapp-internal/license'; import { useLicense } from "@yaakapp-internal/license";
import type { EditorKeymap, Settings } from '@yaakapp-internal/models'; import type { EditorKeymap, Settings } from "@yaakapp-internal/models";
import { patchModel, settingsAtom } from '@yaakapp-internal/models'; import { patchModel, settingsAtom } from "@yaakapp-internal/models";
import { clamp, Heading, HStack, Icon, VStack } from '@yaakapp-internal/ui'; import { clamp, Heading, HStack, Icon, VStack } from "@yaakapp-internal/ui";
import { useAtomValue } from 'jotai'; import { useAtomValue } from "jotai";
import { useState } from 'react'; import { useState } from "react";
import { activeWorkspaceAtom } from '../../hooks/useActiveWorkspace'; import { activeWorkspaceAtom } from "../../hooks/useActiveWorkspace";
import { showConfirm } from '../../lib/confirm'; import { showConfirm } from "../../lib/confirm";
import { invokeCmd } from '../../lib/tauri'; import { invokeCmd } from "../../lib/tauri";
import { CargoFeature } from '../CargoFeature'; import { CargoFeature } from "../CargoFeature";
import { Button } from '../core/Button'; import { Button } from "../core/Button";
import { Checkbox } from '../core/Checkbox'; import { Checkbox } from "../core/Checkbox";
import { Link } from '../core/Link'; import { Link } from "../core/Link";
import { Select } from '../core/Select'; import { Select } from "../core/Select";
const NULL_FONT_VALUE = '__NULL_FONT__'; const NULL_FONT_VALUE = "__NULL_FONT__";
const fontSizeOptions = [ const fontSizeOptions = [
8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30,
].map((n) => ({ label: `${n}`, value: `${n}` })); ].map((n) => ({ label: `${n}`, value: `${n}` }));
const keymaps: { value: EditorKeymap; label: string }[] = [ const keymaps: { value: EditorKeymap; label: string }[] = [
{ value: 'default', label: 'Default' }, { value: "default", label: "Default" },
{ value: 'vim', label: 'Vim' }, { value: "vim", label: "Vim" },
{ value: 'vscode', label: 'VSCode' }, { value: "vscode", label: "VSCode" },
{ value: 'emacs', label: 'Emacs' }, { value: "emacs", label: "Emacs" },
]; ];
export function SettingsInterface() { export function SettingsInterface() {
@@ -50,20 +50,20 @@ export function SettingsInterface() {
help="When opening a workspace, should it open in the current window or a new window?" help="When opening a workspace, should it open in the current window or a new window?"
value={ value={
settings.openWorkspaceNewWindow === true settings.openWorkspaceNewWindow === true
? 'new' ? "new"
: settings.openWorkspaceNewWindow === false : settings.openWorkspaceNewWindow === false
? 'current' ? "current"
: 'ask' : "ask"
} }
onChange={async (v) => { onChange={async (v) => {
if (v === 'current') await patchModel(settings, { openWorkspaceNewWindow: false }); if (v === "current") await patchModel(settings, { openWorkspaceNewWindow: false });
else if (v === 'new') await patchModel(settings, { openWorkspaceNewWindow: true }); else if (v === "new") await patchModel(settings, { openWorkspaceNewWindow: true });
else await patchModel(settings, { openWorkspaceNewWindow: null }); else await patchModel(settings, { openWorkspaceNewWindow: null });
}} }}
options={[ options={[
{ label: 'Always ask', value: 'ask' }, { label: "Always ask", value: "ask" },
{ label: 'Open in current window', value: 'current' }, { label: "Open in current window", value: "current" },
{ label: 'Open in new window', value: 'new' }, { label: "Open in new window", value: "new" },
]} ]}
/> />
<HStack space={2} alignItems="end"> <HStack space={2} alignItems="end">
@@ -74,7 +74,7 @@ export function SettingsInterface() {
label="Interface font" label="Interface font"
value={settings.interfaceFont ?? NULL_FONT_VALUE} value={settings.interfaceFont ?? NULL_FONT_VALUE}
options={[ options={[
{ label: 'System default', value: NULL_FONT_VALUE }, { label: "System default", value: NULL_FONT_VALUE },
...(fonts.data.uiFonts.map((f) => ({ ...(fonts.data.uiFonts.map((f) => ({
label: f, label: f,
value: f, value: f,
@@ -110,7 +110,7 @@ export function SettingsInterface() {
label="Editor font" label="Editor font"
value={settings.editorFont ?? NULL_FONT_VALUE} value={settings.editorFont ?? NULL_FONT_VALUE}
options={[ options={[
{ label: 'System default', value: NULL_FONT_VALUE }, { label: "System default", value: NULL_FONT_VALUE },
...(fonts.data.editorFonts.map((f) => ({ ...(fonts.data.editorFonts.map((f) => ({
label: f, label: f,
value: f, value: f,
@@ -160,7 +160,7 @@ export function SettingsInterface() {
<NativeTitlebarSetting settings={settings} /> <NativeTitlebarSetting settings={settings} />
{type() !== 'macos' && ( {type() !== "macos" && (
<Checkbox <Checkbox
checked={settings.hideWindowControls} checked={settings.hideWindowControls}
title="Hide window controls" title="Hide window controls"
@@ -188,7 +188,7 @@ function NativeTitlebarSetting({ settings }: { settings: Settings }) {
size="2xs" size="2xs"
onClick={async () => { onClick={async () => {
await patchModel(settings, { useNativeTitlebar: nativeTitlebar }); await patchModel(settings, { useNativeTitlebar: nativeTitlebar });
await invokeCmd('cmd_restart'); await invokeCmd("cmd_restart");
}} }}
> >
Apply and Restart Apply and Restart
@@ -200,7 +200,7 @@ function NativeTitlebarSetting({ settings }: { settings: Settings }) {
function LicenseSettings({ settings }: { settings: Settings }) { function LicenseSettings({ settings }: { settings: Settings }) {
const license = useLicense(); const license = useLicense();
if (license.check.data?.status !== 'personal_use') { if (license.check.data?.status !== "personal_use") {
return null; return null;
} }
@@ -211,24 +211,24 @@ function LicenseSettings({ settings }: { settings: Settings }) {
onChange={async (hideLicenseBadge) => { onChange={async (hideLicenseBadge) => {
if (hideLicenseBadge) { if (hideLicenseBadge) {
const confirmed = await showConfirm({ const confirmed = await showConfirm({
id: 'hide-license-badge', id: "hide-license-badge",
title: 'Confirm Personal Use', title: "Confirm Personal Use",
confirmText: 'Confirm', confirmText: "Confirm",
description: ( description: (
<VStack space={3}> <VStack space={3}>
<p>Hey there 👋🏼</p> <p>Hey there 👋🏼</p>
<p> <p>
Yaak is free for personal projects and learning.{' '} Yaak is free for personal projects and learning.{" "}
<strong>If youre using Yaak at work, a license is required.</strong> <strong>If youre using Yaak at work, a license is required.</strong>
</p> </p>
<p> <p>
Licenses help keep Yaak independent and sustainable.{' '} Licenses help keep Yaak independent and sustainable.{" "}
<Link href="https://yaak.app/pricing?s=badge">Purchase a License </Link> <Link href="https://yaak.app/pricing?s=badge">Purchase a License </Link>
</p> </p>
</VStack> </VStack>
), ),
requireTyping: 'Personal Use', requireTyping: "Personal Use",
color: 'info', color: "info",
}); });
if (!confirmed) { if (!confirmed) {
return; // Cancel return; // Cancel

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