mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-29 20:27:03 +02:00
Compare commits
10 Commits
wip/yaak-p
...
codex/cli-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b9b90188a0 | ||
|
|
e64404d7a5 | ||
|
|
072b486857 | ||
|
|
23e9cbb376 | ||
|
|
9c09e32a56 | ||
|
|
be26cc4db4 | ||
|
|
a2f12aef35 | ||
|
|
e34301ccab | ||
|
|
8f0062f917 | ||
|
|
68d68035a1 |
@@ -1,27 +1,23 @@
|
||||
# Claude Context: Detaching Tauri from Yaak
|
||||
|
||||
## 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/`.
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
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, yaak-tauri-utils, etc.)
|
||||
crates-cli/ # CLI crate (yaak-cli)
|
||||
```
|
||||
|
||||
## Completed Work
|
||||
|
||||
### 1. Folder Restructure
|
||||
|
||||
- Moved Tauri-dependent app code to `crates-tauri/yaak-app-client/`
|
||||
- Moved Tauri-dependent app code to `crates-tauri/yaak-app/`
|
||||
- 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
|
||||
|
||||
### 2. Decoupled Crates (no longer depend on Tauri)
|
||||
|
||||
- **yaak-models**: Uses `init_standalone()` pattern for CLI database access
|
||||
- **yaak-http**: Removed Tauri plugin, HttpConnectionManager initialized in yaak-app setup
|
||||
- **yaak-common**: Only contains Tauri-free utilities (serde, platform)
|
||||
@@ -29,7 +25,6 @@ crates-cli/ # CLI crate (yaak-cli)
|
||||
- **yaak-grpc**: Replaced AppHandle with GrpcConfig struct, uses tokio::process::Command instead of Tauri sidecar
|
||||
|
||||
### 3. CLI Implementation
|
||||
|
||||
- Basic CLI at `crates-cli/yaak-cli/src/main.rs`
|
||||
- Commands: workspaces, requests, send (by ID), get (ad-hoc URL), create
|
||||
- Uses same database as Tauri app via `yaak_models::init_standalone()`
|
||||
@@ -37,36 +32,31 @@ crates-cli/ # CLI crate (yaak-cli)
|
||||
## Remaining Work
|
||||
|
||||
### Crates Still Depending on Tauri (in `crates/`)
|
||||
|
||||
1. **yaak-git** (3 files) - Moderate complexity
|
||||
2. **yaak-plugins** (13 files) - **Hardest** - deeply integrated with Tauri for plugin-to-window communication
|
||||
3. **yaak-sync** (4 files) - Moderate complexity
|
||||
4. **yaak-ws** (5 files) - Moderate complexity
|
||||
|
||||
### Pattern for Decoupling
|
||||
|
||||
1. Remove Tauri plugin `init()` function from the crate
|
||||
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
|
||||
4. Initialize managers in yaak-app's `.setup()` block
|
||||
5. Remove `tauri` from Cargo.toml dependencies
|
||||
6. Update `crates-tauri/yaak-app-client/capabilities/default.json` to remove the plugin permission
|
||||
6. Update `crates-tauri/yaak-app/capabilities/default.json` to remove the plugin permission
|
||||
7. Replace `tauri::async_runtime::block_on` with `tokio::runtime::Handle::current().block_on()`
|
||||
|
||||
## Key Files
|
||||
|
||||
- `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/models_ext.rs` - Database plugin and extension traits
|
||||
- `crates-tauri/yaak-app/src/lib.rs` - Main Tauri app, setup block initializes managers
|
||||
- `crates-tauri/yaak-app/src/commands.rs` - Migrated Tauri commands
|
||||
- `crates-tauri/yaak-app/src/models_ext.rs` - Database plugin and extension traits
|
||||
- `crates-tauri/yaak-tauri-utils/src/window.rs` - WorkspaceWindowTrait for window state
|
||||
- `crates/yaak-models/src/lib.rs` - Contains `init_standalone()` for CLI usage
|
||||
|
||||
## Git Branch
|
||||
|
||||
Working on `detach-tauri` branch.
|
||||
|
||||
## Recent Commits
|
||||
|
||||
```
|
||||
c40cff40 Remove Tauri dependencies from yaak-crypto and yaak-grpc
|
||||
df495f1d Move Tauri utilities from yaak-common to yaak-tauri-utils
|
||||
@@ -77,7 +67,6 @@ e718a5f1 Refactor models_ext to use init_standalone from yaak-models
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
- 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 app-dev` to test the Tauri app still works
|
||||
- Run `cargo run -p yaak-cli -- --help` to test the CLI
|
||||
|
||||
62
.claude/commands/release/check-out-pr.md
Normal file
62
.claude/commands/release/check-out-pr.md
Normal file
@@ -0,0 +1,62 @@
|
||||
---
|
||||
description: Review a PR in a new worktree
|
||||
allowed-tools: Bash(git worktree:*), Bash(gh pr:*), Bash(git branch:*)
|
||||
---
|
||||
|
||||
Check out a GitHub pull request for review.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/check-out-pr <PR_NUMBER>
|
||||
```
|
||||
|
||||
## What to do
|
||||
|
||||
1. If no PR number is provided, list all open pull requests and ask the user to select one
|
||||
2. Get PR information using `gh pr view <PR_NUMBER> --json number,headRefName`
|
||||
3. **Ask the user** whether they want to:
|
||||
- **A) Check out in current directory** — simple `gh pr checkout <PR_NUMBER>`
|
||||
- **B) Create a new worktree** — isolated copy at `../yaak-worktrees/pr-<PR_NUMBER>`
|
||||
4. Follow the appropriate path below
|
||||
|
||||
## Option A: Check out in current directory
|
||||
|
||||
1. Run `gh pr checkout <PR_NUMBER>`
|
||||
2. Inform the user which branch they're now on
|
||||
|
||||
## Option B: Create a new worktree
|
||||
|
||||
1. Create a new worktree at `../yaak-worktrees/pr-<PR_NUMBER>` using `git worktree add` with a timeout of at least 300000ms (5 minutes) since the post-checkout hook runs a bootstrap script
|
||||
2. Checkout the PR branch in the new worktree using `gh pr checkout <PR_NUMBER>`
|
||||
3. The post-checkout hook will automatically:
|
||||
- Create `.env.local` with unique ports
|
||||
- Copy editor config folders
|
||||
- Run `npm install && npm run bootstrap`
|
||||
4. Inform the user:
|
||||
- Where the worktree was created
|
||||
- What ports were assigned
|
||||
- How to access it (cd command)
|
||||
- How to run the dev server
|
||||
- How to remove the worktree when done
|
||||
|
||||
### Example worktree output
|
||||
|
||||
```
|
||||
Created worktree for PR #123 at ../yaak-worktrees/pr-123
|
||||
Branch: feature-auth
|
||||
Ports: Vite (1421), MCP (64344)
|
||||
|
||||
To start working:
|
||||
cd ../yaak-worktrees/pr-123
|
||||
npm run app-dev
|
||||
|
||||
To remove when done:
|
||||
git worktree remove ../yaak-worktrees/pr-123
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
- If the PR doesn't exist, show a helpful error
|
||||
- If the worktree already exists, inform the user and ask if they want to remove and recreate it
|
||||
- If `gh` CLI is not available, inform the user to install it
|
||||
@@ -8,7 +8,7 @@ Generate formatted release notes for Yaak releases by analyzing git history and
|
||||
## What to do
|
||||
|
||||
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 stable version, it retrieves commits between the stable version and the previous stable version
|
||||
3. Fetches PR descriptions for linked issues to find:
|
||||
@@ -37,7 +37,6 @@ The skill generates markdown-formatted release notes following this structure:
|
||||
|
||||
**IMPORTANT**: Always add a blank lines around the markdown code fence and output the markdown code block last
|
||||
**IMPORTANT**: PRs by `@gschier` should not mention the @username
|
||||
**IMPORTANT**: These are app release notes. Exclude CLI-only changes (commits prefixed with `cli:` or only touching `crates-cli/`) since the CLI has its own release process.
|
||||
|
||||
## After Generating Release Notes
|
||||
|
||||
|
||||
35
.claude/skills/worktree.md
Normal file
35
.claude/skills/worktree.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Worktree Management Skill
|
||||
|
||||
## Creating Worktrees
|
||||
|
||||
When creating git worktrees for this project, ALWAYS use the path format:
|
||||
```
|
||||
../yaak-worktrees/<NAME>
|
||||
```
|
||||
|
||||
For example:
|
||||
- `git worktree add ../yaak-worktrees/feature-auth`
|
||||
- `git worktree add ../yaak-worktrees/bugfix-login`
|
||||
- `git worktree add ../yaak-worktrees/refactor-api`
|
||||
|
||||
## What Happens Automatically
|
||||
|
||||
The post-checkout hook will automatically:
|
||||
1. Create `.env.local` with unique ports (YAAK_DEV_PORT and YAAK_PLUGIN_MCP_SERVER_PORT)
|
||||
2. Copy gitignored editor config folders (.zed, .idea, etc.)
|
||||
3. Run `npm install && npm run bootstrap`
|
||||
|
||||
## Deleting Worktrees
|
||||
|
||||
```bash
|
||||
git worktree remove ../yaak-worktrees/<NAME>
|
||||
```
|
||||
|
||||
## Port Assignments
|
||||
|
||||
- Main worktree: 1420 (Vite), 64343 (MCP)
|
||||
- First worktree: 1421, 64344
|
||||
- Second worktree: 1422, 64345
|
||||
- etc.
|
||||
|
||||
Each worktree can run `npm run app-dev` simultaneously without conflicts.
|
||||
46
.codex/skills/release-check-out-pr/SKILL.md
Normal file
46
.codex/skills/release-check-out-pr/SKILL.md
Normal file
@@ -0,0 +1,46 @@
|
||||
---
|
||||
name: release-check-out-pr
|
||||
description: Check out a GitHub pull request for review in this repo, either in the current directory or in a new isolated worktree at ../yaak-worktrees/pr-<PR_NUMBER>. Use when asked to run or replace the old Claude check-out-pr command.
|
||||
---
|
||||
|
||||
# Check Out PR
|
||||
|
||||
Check out a PR by number and let the user choose between current-directory checkout and isolated worktree checkout.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Confirm `gh` CLI is available.
|
||||
2. If no PR number is provided, list open PRs (`gh pr list`) and ask the user to choose one.
|
||||
3. Read PR metadata:
|
||||
- `gh pr view <PR_NUMBER> --json number,headRefName`
|
||||
4. Ask the user to choose:
|
||||
- Option A: check out in the current directory
|
||||
- Option B: create a new worktree at `../yaak-worktrees/pr-<PR_NUMBER>`
|
||||
|
||||
## Option A: Current Directory
|
||||
|
||||
1. Run:
|
||||
- `gh pr checkout <PR_NUMBER>`
|
||||
2. Report the checked-out branch.
|
||||
|
||||
## Option B: New Worktree
|
||||
|
||||
1. Use path:
|
||||
- `../yaak-worktrees/pr-<PR_NUMBER>`
|
||||
2. Create the worktree with a timeout of at least 5 minutes because checkout hooks run bootstrap.
|
||||
3. In the new worktree, run:
|
||||
- `gh pr checkout <PR_NUMBER>`
|
||||
4. Report:
|
||||
- Worktree path
|
||||
- Assigned ports from `.env.local` if present
|
||||
- How to start work:
|
||||
- `cd ../yaak-worktrees/pr-<PR_NUMBER>`
|
||||
- `npm run app-dev`
|
||||
- How to remove when done:
|
||||
- `git worktree remove ../yaak-worktrees/pr-<PR_NUMBER>`
|
||||
|
||||
## Error Handling
|
||||
|
||||
- If PR does not exist, show a clear error.
|
||||
- If worktree already exists, ask whether to reuse it or remove/recreate it.
|
||||
- If `gh` is missing, instruct the user to install/authenticate it.
|
||||
@@ -32,7 +32,6 @@ Generate formatted markdown release notes for a Yaak tag.
|
||||
- Keep a blank line before and after the code fence.
|
||||
- Output the markdown code block last.
|
||||
- Do not append `by @gschier` for PRs authored by `@gschier`.
|
||||
- These are app release notes. Exclude CLI-only changes (commits prefixed with `cli:` or only touching `crates-cli/`) since the CLI has its own release process.
|
||||
|
||||
## Release Creation Prompt
|
||||
|
||||
|
||||
37
.codex/skills/worktree-management/SKILL.md
Normal file
37
.codex/skills/worktree-management/SKILL.md
Normal file
@@ -0,0 +1,37 @@
|
||||
---
|
||||
name: worktree-management
|
||||
description: Manage Yaak git worktrees using the standard ../yaak-worktrees/<NAME> layout, including creation, removal, and expected automatic setup behavior and port assignments.
|
||||
---
|
||||
|
||||
# Worktree Management
|
||||
|
||||
Use the Yaak-standard worktree path layout and lifecycle commands.
|
||||
|
||||
## Path Convention
|
||||
|
||||
Always create worktrees under:
|
||||
|
||||
`../yaak-worktrees/<NAME>`
|
||||
|
||||
Examples:
|
||||
- `git worktree add ../yaak-worktrees/feature-auth`
|
||||
- `git worktree add ../yaak-worktrees/bugfix-login`
|
||||
- `git worktree add ../yaak-worktrees/refactor-api`
|
||||
|
||||
## Automatic Setup After Checkout
|
||||
|
||||
Project git hooks automatically:
|
||||
1. Create `.env.local` with unique `YAAK_DEV_PORT` and `YAAK_PLUGIN_MCP_SERVER_PORT`
|
||||
2. Copy gitignored editor config folders
|
||||
3. Run `npm install && npm run bootstrap`
|
||||
|
||||
## Remove Worktree
|
||||
|
||||
`git worktree remove ../yaak-worktrees/<NAME>`
|
||||
|
||||
## Port Pattern
|
||||
|
||||
- Main worktree: Vite `1420`, MCP `64343`
|
||||
- First extra worktree: `1421`, `64344`
|
||||
- Second extra worktree: `1422`, `64345`
|
||||
- Continue incrementally for additional worktrees
|
||||
4
.gitattributes
vendored
4
.gitattributes
vendored
@@ -1,5 +1,5 @@
|
||||
crates-tauri/yaak-app-client/vendored/**/* linguist-generated=true
|
||||
crates-tauri/yaak-app-client/gen/schemas/**/* linguist-generated=true
|
||||
crates-tauri/yaak-app/vendored/**/* linguist-generated=true
|
||||
crates-tauri/yaak-app/gen/schemas/**/* linguist-generated=true
|
||||
**/bindings/* linguist-generated=true
|
||||
crates/yaak-templates/pkg/* linguist-generated=true
|
||||
|
||||
|
||||
24
.github/ISSUE_TEMPLATE/bug_report.md
vendored
24
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,9 +1,10 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ""
|
||||
labels: ""
|
||||
assignees: ""
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
@@ -11,7 +12,6 @@ A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
@@ -24,17 +24,15 @@ A clear and concise description of what you expected to happen.
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Version [e.g. 22]
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Smartphone (please complete the following information):**
|
||||
|
||||
- Device: [e.g. iPhone6]
|
||||
- OS: [e.g. iOS8.1]
|
||||
- Browser [e.g. stock browser, safari]
|
||||
- Version [e.g. 22]
|
||||
- Device: [e.g. iPhone6]
|
||||
- OS: [e.g. iOS8.1]
|
||||
- Browser [e.g. stock browser, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
|
||||
1
.github/pull_request_template.md
vendored
1
.github/pull_request_template.md
vendored
@@ -11,7 +11,6 @@
|
||||
- [ ] I added or updated tests when reasonable.
|
||||
|
||||
Approved feedback item (required if not a bug fix or small-scope improvement):
|
||||
|
||||
<!-- https://yaak.app/feedback/... -->
|
||||
|
||||
## Related
|
||||
|
||||
9
.github/workflows/ci.yml
vendored
9
.github/workflows/ci.yml
vendored
@@ -14,20 +14,17 @@ jobs:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: voidzero-dev/setup-vp@v1
|
||||
with:
|
||||
node-version: "24"
|
||||
cache: true
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
shared-key: ci
|
||||
cache-on-failure: true
|
||||
|
||||
- run: vp install
|
||||
- run: npm ci
|
||||
- run: npm run bootstrap
|
||||
- run: npm run lint
|
||||
- name: Run JS Tests
|
||||
run: vp test
|
||||
run: npm test
|
||||
- name: Run Rust Tests
|
||||
run: cargo test --all
|
||||
|
||||
1
.github/workflows/claude.yml
vendored
1
.github/workflows/claude.yml
vendored
@@ -47,3 +47,4 @@ jobs:
|
||||
# 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
|
||||
# claude_args: '--allowed-tools Bash(gh pr:*)'
|
||||
|
||||
|
||||
21
.github/workflows/release-app.yml
vendored
21
.github/workflows/release-app.yml
vendored
@@ -50,11 +50,8 @@ jobs:
|
||||
- name: Checkout yaakapp/app
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Vite+
|
||||
uses: voidzero-dev/setup-vp@v1
|
||||
with:
|
||||
node-version: "24"
|
||||
cache: true
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
|
||||
- name: install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
@@ -90,15 +87,15 @@ jobs:
|
||||
echo $dir >> $env:GITHUB_PATH
|
||||
& $exe --version
|
||||
|
||||
- run: vp install
|
||||
- run: npm ci
|
||||
- run: npm run bootstrap
|
||||
env:
|
||||
YAAK_TARGET_ARCH: ${{ matrix.yaak_arch }}
|
||||
- run: npm run lint
|
||||
- name: Run JS Tests
|
||||
run: vp test
|
||||
run: npm test
|
||||
- name: Run Rust Tests
|
||||
run: cargo test --all --exclude yaak-cli
|
||||
run: cargo test --all
|
||||
|
||||
- name: Set version
|
||||
run: npm run replace-version
|
||||
@@ -125,8 +122,8 @@ jobs:
|
||||
security list-keychain -d user -s $KEYCHAIN_PATH
|
||||
|
||||
# Sign vendored binaries with hardened runtime and their specific entitlements
|
||||
codesign --force --options runtime --entitlements crates-tauri/yaak-app-client/macos/entitlements.yaakprotoc.plist --sign "$APPLE_SIGNING_IDENTITY" crates-tauri/yaak-app-client/vendored/protoc/yaakprotoc || true
|
||||
codesign --force --options runtime --entitlements crates-tauri/yaak-app-client/macos/entitlements.yaaknode.plist --sign "$APPLE_SIGNING_IDENTITY" crates-tauri/yaak-app-client/vendored/node/yaaknode || true
|
||||
codesign --force --options runtime --entitlements crates-tauri/yaak-app/macos/entitlements.yaakprotoc.plist --sign "$APPLE_SIGNING_IDENTITY" crates-tauri/yaak-app/vendored/protoc/yaakprotoc || true
|
||||
codesign --force --options runtime --entitlements crates-tauri/yaak-app/macos/entitlements.yaaknode.plist --sign "$APPLE_SIGNING_IDENTITY" crates-tauri/yaak-app/vendored/node/yaaknode || true
|
||||
|
||||
- uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
@@ -155,7 +152,7 @@ jobs:
|
||||
releaseBody: "[Changelog __VERSION__](https://yaak.app/blog/__VERSION__)"
|
||||
releaseDraft: true
|
||||
prerelease: true
|
||||
args: "${{ matrix.args }} --config ./crates-tauri/yaak-app-client/tauri.release.conf.json"
|
||||
args: "${{ matrix.args }} --config ./crates-tauri/yaak-app/tauri.release.conf.json"
|
||||
|
||||
# Build a per-machine NSIS installer for enterprise deployment (PDQ, SCCM, Intune)
|
||||
- name: Build and upload machine-wide installer (Windows only)
|
||||
@@ -171,7 +168,7 @@ jobs:
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
run: |
|
||||
Get-ChildItem -Recurse -Path target -File -Filter "*.exe.sig" | Remove-Item -Force
|
||||
npx tauri bundle ${{ matrix.args }} --bundles nsis --config ./crates-tauri/yaak-app-client/tauri.release.conf.json --config '{"bundle":{"createUpdaterArtifacts":true,"windows":{"nsis":{"installMode":"perMachine"}}}}'
|
||||
npx tauri bundle ${{ matrix.args }} --bundles nsis --config ./crates-tauri/yaak-app/tauri.release.conf.json --config '{"bundle":{"createUpdaterArtifacts":true,"windows":{"nsis":{"installMode":"perMachine"}}}}'
|
||||
$setup = Get-ChildItem -Recurse -Path target -Filter "*setup*.exe" | Select-Object -First 1
|
||||
$setupSig = "$($setup.FullName).sig"
|
||||
$dest = $setup.FullName -replace '-setup\.exe$', '-setup-machine.exe'
|
||||
|
||||
6
.github/workflows/release-cli-npm.yml
vendored
6
.github/workflows/release-cli-npm.yml
vendored
@@ -45,8 +45,8 @@ jobs:
|
||||
with:
|
||||
name: vendored-assets
|
||||
path: |
|
||||
crates-tauri/yaak-app-client/vendored/plugin-runtime/index.cjs
|
||||
crates-tauri/yaak-app-client/vendored/plugins
|
||||
crates-tauri/yaak-app/vendored/plugin-runtime/index.cjs
|
||||
crates-tauri/yaak-app/vendored/plugins
|
||||
if-no-files-found: error
|
||||
|
||||
build-binaries:
|
||||
@@ -107,7 +107,7 @@ jobs:
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: vendored-assets
|
||||
path: crates-tauri/yaak-app-client/vendored
|
||||
path: crates-tauri/yaak-app/vendored
|
||||
|
||||
- name: Set CLI build version
|
||||
shell: bash
|
||||
|
||||
10
.github/workflows/sponsors.yml
vendored
10
.github/workflows/sponsors.yml
vendored
@@ -16,23 +16,23 @@ jobs:
|
||||
uses: JamesIves/github-sponsors-readme-action@v1
|
||||
with:
|
||||
token: ${{ secrets.SPONSORS_PAT }}
|
||||
file: "README.md"
|
||||
file: 'README.md'
|
||||
maximum: 1999
|
||||
template: '<a href="https://github.com/{{{ login }}}"><img src="{{{ avatarUrl }}}" width="50px" alt="User avatar: {{{ login }}}" /></a> '
|
||||
active-only: false
|
||||
include-private: true
|
||||
marker: "sponsors-base"
|
||||
marker: 'sponsors-base'
|
||||
|
||||
- name: Generate Sponsors
|
||||
uses: JamesIves/github-sponsors-readme-action@v1
|
||||
with:
|
||||
token: ${{ secrets.SPONSORS_PAT }}
|
||||
file: "README.md"
|
||||
file: 'README.md'
|
||||
minimum: 2000
|
||||
template: '<a href="https://github.com/{{{ login }}}"><img src="{{{ avatarUrl }}}" width="80px" alt="User avatar: {{{ login }}}" /></a> '
|
||||
active-only: false
|
||||
include-private: true
|
||||
marker: "sponsors-premium"
|
||||
marker: 'sponsors-premium'
|
||||
|
||||
# ⚠️ Note: You can use any deployment step here to automatically push the README
|
||||
# changes back to your branch.
|
||||
@@ -41,4 +41,4 @@ jobs:
|
||||
with:
|
||||
branch: main
|
||||
force: false
|
||||
folder: "."
|
||||
folder: '.'
|
||||
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -39,8 +39,7 @@ codebook.toml
|
||||
target
|
||||
|
||||
# Per-worktree Tauri config (generated by post-checkout hook)
|
||||
crates-tauri/yaak-app-client/tauri.worktree.conf.json
|
||||
crates-tauri/yaak-app-proxy/tauri.worktree.conf.json
|
||||
crates-tauri/yaak-app/tauri.worktree.conf.json
|
||||
|
||||
# Tauri auto-generated permission files
|
||||
**/permissions/autogenerated
|
||||
@@ -55,6 +54,3 @@ flatpak/node-sources.json
|
||||
|
||||
# Local Codex desktop env state
|
||||
.codex/environments/environment.toml
|
||||
|
||||
# Claude Code local settings
|
||||
.claude/settings.local.json
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
24.14.0
|
||||
2
.npmrc
2
.npmrc
@@ -1,2 +0,0 @@
|
||||
# vite-plugin-wasm has not yet declared Vite 8 in its peerDependencies
|
||||
legacy-peer-deps=true
|
||||
@@ -1,3 +0,0 @@
|
||||
**/bindings/**
|
||||
**/routeTree.gen.ts
|
||||
crates/yaak-templates/pkg/**
|
||||
@@ -1 +0,0 @@
|
||||
vp lint
|
||||
6
.vscode/extensions.json
vendored
6
.vscode/extensions.json
vendored
@@ -1,7 +1,3 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"rust-lang.rust-analyzer",
|
||||
"bradlc.vscode-tailwindcss",
|
||||
"VoidZero.vite-plus-extension-pack"
|
||||
]
|
||||
"recommendations": ["biomejs.biome", "rust-lang.rust-analyzer", "bradlc.vscode-tailwindcss"]
|
||||
}
|
||||
|
||||
8
.vscode/settings.json
vendored
8
.vscode/settings.json
vendored
@@ -1,8 +1,6 @@
|
||||
{
|
||||
"editor.defaultFormatter": "oxc.oxc-vscode",
|
||||
"editor.defaultFormatter": "biomejs.biome",
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnSaveMode": "file",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.oxc": "explicit"
|
||||
}
|
||||
"biome.enabled": true,
|
||||
"biome.lint.format.enable": true
|
||||
}
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
- Tag safety: app releases use `v*` tags and CLI releases use `yaak-cli-*` tags; always confirm which one is requested before retagging.
|
||||
- Do not commit, push, or tag without explicit approval
|
||||
397
Cargo.lock
generated
397
Cargo.lock
generated
@@ -173,17 +173,6 @@ version = "1.0.98"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
|
||||
|
||||
[[package]]
|
||||
name = "apollo-parser"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "947e21ff51879f8a40d7519dfe619268de2afba4042a8a43878276de3cb910f0"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"rowan",
|
||||
"thiserror 2.0.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "append-only-vec"
|
||||
version = "0.1.8"
|
||||
@@ -488,28 +477,6 @@ version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
|
||||
|
||||
[[package]]
|
||||
name = "aws-lc-rs"
|
||||
version = "1.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94bffc006df10ac2a68c83692d734a465f8ee6c5b384d8545a636f81d858f4bf"
|
||||
dependencies = [
|
||||
"aws-lc-sys",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-lc-sys"
|
||||
version = "0.38.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4321e568ed89bb5a7d291a7f37997c2c0df89809d7b6d12062c81ddb54aa782e"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cmake",
|
||||
"dunce",
|
||||
"fs_extra",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "axum"
|
||||
version = "0.7.9"
|
||||
@@ -1233,7 +1200,7 @@ dependencies = [
|
||||
"encode_unicode",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"unicode-width 0.2.2",
|
||||
"unicode-width",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
@@ -1380,12 +1347,6 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "countme"
|
||||
version = "3.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636"
|
||||
|
||||
[[package]]
|
||||
name = "cow-utils"
|
||||
version = "0.1.3"
|
||||
@@ -1444,31 +1405,6 @@ version = "0.8.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
||||
|
||||
[[package]]
|
||||
name = "crossterm"
|
||||
version = "0.25.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"crossterm_winapi",
|
||||
"libc",
|
||||
"mio 0.8.11",
|
||||
"parking_lot",
|
||||
"signal-hook",
|
||||
"signal-hook-mio",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossterm_winapi"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crunchy"
|
||||
version = "0.2.3"
|
||||
@@ -2231,12 +2167,6 @@ dependencies = [
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fs_extra"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
|
||||
|
||||
[[package]]
|
||||
name = "fsevent-sys"
|
||||
version = "4.1.0"
|
||||
@@ -2364,15 +2294,6 @@ dependencies = [
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fuzzy-matcher"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94"
|
||||
dependencies = [
|
||||
"thread_local 1.1.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fxhash"
|
||||
version = "0.2.1"
|
||||
@@ -3243,24 +3164,6 @@ dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inquire"
|
||||
version = "0.7.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fddf93031af70e75410a2511ec04d49e758ed2f26dad3404a934e0fb45cc12a"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"crossterm",
|
||||
"dyn-clone",
|
||||
"fuzzy-matcher",
|
||||
"fxhash",
|
||||
"newline-converter",
|
||||
"once_cell",
|
||||
"tempfile",
|
||||
"unicode-segmentation",
|
||||
"unicode-width 0.1.14",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "interfaces"
|
||||
version = "0.0.8"
|
||||
@@ -3853,18 +3756,6 @@ dependencies = [
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log 0.4.29",
|
||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.0.4"
|
||||
@@ -3960,15 +3851,6 @@ version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
|
||||
|
||||
[[package]]
|
||||
name = "newline-converter"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47b6b097ecb1cbfed438542d16e84fd7ad9b0c76c8a65b7f9039212a3d14dc7f"
|
||||
dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nibble_vec"
|
||||
version = "0.1.0"
|
||||
@@ -4060,7 +3942,7 @@ dependencies = [
|
||||
"kqueue",
|
||||
"libc",
|
||||
"log 0.4.29",
|
||||
"mio 1.0.4",
|
||||
"mio",
|
||||
"notify-types",
|
||||
"walkdir",
|
||||
"windows-sys 0.59.0",
|
||||
@@ -4601,7 +4483,7 @@ checksum = "75b1853bc34cadaa90aa09f95713d8b77ec0c0d3e2d90ccf7a74216f40d20850"
|
||||
dependencies = [
|
||||
"flate2",
|
||||
"postcard",
|
||||
"rustc-hash 2.1.1",
|
||||
"rustc-hash",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.17",
|
||||
@@ -4619,7 +4501,7 @@ dependencies = [
|
||||
"textwrap",
|
||||
"thiserror 2.0.17",
|
||||
"unicode-segmentation",
|
||||
"unicode-width 0.2.2",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4644,7 +4526,7 @@ dependencies = [
|
||||
"hashbrown 0.16.1",
|
||||
"oxc_data_structures",
|
||||
"oxc_estree",
|
||||
"rustc-hash 2.1.1",
|
||||
"rustc-hash",
|
||||
"serde",
|
||||
]
|
||||
|
||||
@@ -4700,7 +4582,7 @@ dependencies = [
|
||||
"oxc_index",
|
||||
"oxc_syntax",
|
||||
"petgraph 0.8.3",
|
||||
"rustc-hash 2.1.1",
|
||||
"rustc-hash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4721,7 +4603,7 @@ dependencies = [
|
||||
"oxc_sourcemap",
|
||||
"oxc_span",
|
||||
"oxc_syntax",
|
||||
"rustc-hash 2.1.1",
|
||||
"rustc-hash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4733,7 +4615,7 @@ dependencies = [
|
||||
"cow-utils",
|
||||
"oxc-browserslist",
|
||||
"oxc_syntax",
|
||||
"rustc-hash 2.1.1",
|
||||
"rustc-hash",
|
||||
"serde",
|
||||
]
|
||||
|
||||
@@ -4808,7 +4690,7 @@ dependencies = [
|
||||
"oxc_ecmascript",
|
||||
"oxc_span",
|
||||
"oxc_syntax",
|
||||
"rustc-hash 2.1.1",
|
||||
"rustc-hash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4825,7 +4707,7 @@ dependencies = [
|
||||
"oxc_semantic",
|
||||
"oxc_span",
|
||||
"oxc_syntax",
|
||||
"rustc-hash 2.1.1",
|
||||
"rustc-hash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4850,7 +4732,7 @@ dependencies = [
|
||||
"oxc_span",
|
||||
"oxc_syntax",
|
||||
"oxc_traverse",
|
||||
"rustc-hash 2.1.1",
|
||||
"rustc-hash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4872,7 +4754,7 @@ dependencies = [
|
||||
"oxc_regular_expression",
|
||||
"oxc_span",
|
||||
"oxc_syntax",
|
||||
"rustc-hash 2.1.1",
|
||||
"rustc-hash",
|
||||
"seq-macro",
|
||||
]
|
||||
|
||||
@@ -4888,7 +4770,7 @@ dependencies = [
|
||||
"oxc_diagnostics",
|
||||
"oxc_span",
|
||||
"phf 0.13.1",
|
||||
"rustc-hash 2.1.1",
|
||||
"rustc-hash",
|
||||
"unicode-id-start",
|
||||
]
|
||||
|
||||
@@ -4905,7 +4787,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"papaya",
|
||||
"pnp",
|
||||
"rustc-hash 2.1.1",
|
||||
"rustc-hash",
|
||||
"self_cell",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -4935,7 +4817,7 @@ dependencies = [
|
||||
"oxc_span",
|
||||
"oxc_syntax",
|
||||
"phf 0.13.1",
|
||||
"rustc-hash 2.1.1",
|
||||
"rustc-hash",
|
||||
"self_cell",
|
||||
]
|
||||
|
||||
@@ -4947,7 +4829,7 @@ checksum = "c7f89482522f3cd820817d48ee4ade5b10822060d6e5e4d419f05f6d8bd29d70"
|
||||
dependencies = [
|
||||
"base64-simd",
|
||||
"json-escape-simd",
|
||||
"rustc-hash 2.1.1",
|
||||
"rustc-hash",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
@@ -5011,7 +4893,7 @@ dependencies = [
|
||||
"oxc_span",
|
||||
"oxc_syntax",
|
||||
"oxc_traverse",
|
||||
"rustc-hash 2.1.1",
|
||||
"rustc-hash",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha1",
|
||||
@@ -5036,7 +4918,7 @@ dependencies = [
|
||||
"oxc_syntax",
|
||||
"oxc_transformer",
|
||||
"oxc_traverse",
|
||||
"rustc-hash 2.1.1",
|
||||
"rustc-hash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5054,7 +4936,7 @@ dependencies = [
|
||||
"oxc_semantic",
|
||||
"oxc_span",
|
||||
"oxc_syntax",
|
||||
"rustc-hash 2.1.1",
|
||||
"rustc-hash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5160,16 +5042,6 @@ dependencies = [
|
||||
"hmac",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pem"
|
||||
version = "3.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.2"
|
||||
@@ -5469,7 +5341,7 @@ dependencies = [
|
||||
"nodejs-built-in-modules",
|
||||
"pathdiff",
|
||||
"radix_trie",
|
||||
"rustc-hash 2.1.1",
|
||||
"rustc-hash",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.17",
|
||||
@@ -5588,18 +5460,6 @@ dependencies = [
|
||||
"termtree",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pretty_graphql"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea8c38ecedb3d28a998ea783469a78587f5f984d61226cf071f6979861e9e6a9"
|
||||
dependencies = [
|
||||
"apollo-parser",
|
||||
"memchr",
|
||||
"rowan",
|
||||
"tiny_pretty",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-crate"
|
||||
version = "1.3.1"
|
||||
@@ -5780,7 +5640,7 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
"quinn-proto",
|
||||
"quinn-udp",
|
||||
"rustc-hash 2.1.1",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"socket2 0.5.10",
|
||||
"thiserror 2.0.17",
|
||||
@@ -5800,7 +5660,7 @@ dependencies = [
|
||||
"lru-slab",
|
||||
"rand 0.9.1",
|
||||
"ring",
|
||||
"rustc-hash 2.1.1",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"slab",
|
||||
@@ -6022,19 +5882,6 @@ dependencies = [
|
||||
"cipher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rcgen"
|
||||
version = "0.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2"
|
||||
dependencies = [
|
||||
"pem",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"time",
|
||||
"yasna",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.12"
|
||||
@@ -6307,7 +6154,7 @@ dependencies = [
|
||||
"rolldown_tracing",
|
||||
"rolldown_utils",
|
||||
"rolldown_watcher",
|
||||
"rustc-hash 2.1.1",
|
||||
"rustc-hash",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"string_wizard",
|
||||
@@ -6324,7 +6171,7 @@ version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77dff57c9de498bb1eb5b1ce682c2e3a0ae956b266fa0933c3e151b87b078967"
|
||||
dependencies = [
|
||||
"unicode-width 0.2.2",
|
||||
"unicode-width",
|
||||
"yansi",
|
||||
]
|
||||
|
||||
@@ -6349,7 +6196,7 @@ dependencies = [
|
||||
"kqueue",
|
||||
"libc",
|
||||
"log 0.4.29",
|
||||
"mio 1.0.4",
|
||||
"mio",
|
||||
"rolldown-notify-types",
|
||||
"walkdir",
|
||||
"windows-sys 0.61.2",
|
||||
@@ -6397,7 +6244,7 @@ dependencies = [
|
||||
"rolldown_sourcemap",
|
||||
"rolldown_std_utils",
|
||||
"rolldown_utils",
|
||||
"rustc-hash 2.1.1",
|
||||
"rustc-hash",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"simdutf8",
|
||||
@@ -6415,7 +6262,7 @@ dependencies = [
|
||||
"blake3",
|
||||
"dashmap",
|
||||
"rolldown_debug_action",
|
||||
"rustc-hash 2.1.1",
|
||||
"rustc-hash",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tracing",
|
||||
@@ -6472,7 +6319,7 @@ dependencies = [
|
||||
"rolldown-ariadne",
|
||||
"rolldown_utils",
|
||||
"ropey",
|
||||
"rustc-hash 2.1.1",
|
||||
"rustc-hash",
|
||||
"sugar_path",
|
||||
]
|
||||
|
||||
@@ -6506,7 +6353,7 @@ dependencies = [
|
||||
"rolldown_resolver",
|
||||
"rolldown_sourcemap",
|
||||
"rolldown_utils",
|
||||
"rustc-hash 2.1.1",
|
||||
"rustc-hash",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"string_wizard",
|
||||
@@ -6526,7 +6373,7 @@ dependencies = [
|
||||
"rolldown_common",
|
||||
"rolldown_plugin",
|
||||
"rolldown_utils",
|
||||
"rustc-hash 2.1.1",
|
||||
"rustc-hash",
|
||||
"serde_json",
|
||||
"xxhash-rust",
|
||||
]
|
||||
@@ -6597,7 +6444,7 @@ dependencies = [
|
||||
"oxc",
|
||||
"oxc_sourcemap",
|
||||
"rolldown_utils",
|
||||
"rustc-hash 2.1.1",
|
||||
"rustc-hash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6649,7 +6496,7 @@ dependencies = [
|
||||
"regex 1.11.1",
|
||||
"regress",
|
||||
"rolldown_std_utils",
|
||||
"rustc-hash 2.1.1",
|
||||
"rustc-hash",
|
||||
"serde_json",
|
||||
"simdutf8",
|
||||
"sugar_path",
|
||||
@@ -6679,18 +6526,6 @@ dependencies = [
|
||||
"str_indices",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rowan"
|
||||
version = "0.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "417a3a9f582e349834051b8a10c8d71ca88da4211e4093528e36b9845f6b5f21"
|
||||
dependencies = [
|
||||
"countme",
|
||||
"hashbrown 0.14.5",
|
||||
"rustc-hash 1.1.0",
|
||||
"text-size",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rusqlite"
|
||||
version = "0.32.1"
|
||||
@@ -6733,12 +6568,6 @@ dependencies = [
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "2.1.1"
|
||||
@@ -6786,8 +6615,6 @@ version = "0.23.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a9586e9ee2b4f8fab52a0048ca7334d7024eef48e2cb9407e3497bb7cab7fa7"
|
||||
dependencies = [
|
||||
"aws-lc-rs",
|
||||
"log 0.4.29",
|
||||
"once_cell",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
@@ -6860,7 +6687,6 @@ version = "0.103.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf"
|
||||
dependencies = [
|
||||
"aws-lc-rs",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"untrusted",
|
||||
@@ -7347,27 +7173,6 @@ version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook"
|
||||
version = "0.3.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"signal-hook-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-mio"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"mio 0.8.11",
|
||||
"signal-hook",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.5"
|
||||
@@ -7554,7 +7359,7 @@ dependencies = [
|
||||
"memchr",
|
||||
"oxc_index",
|
||||
"oxc_sourcemap",
|
||||
"rustc-hash 2.1.1",
|
||||
"rustc-hash",
|
||||
"serde",
|
||||
]
|
||||
|
||||
@@ -8255,12 +8060,6 @@ version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683"
|
||||
|
||||
[[package]]
|
||||
name = "text-size"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f18aa187839b2bdb1ad2fa35ead8c4c2976b64e4363c386d45ac0f7ee85c9233"
|
||||
|
||||
[[package]]
|
||||
name = "textwrap"
|
||||
version = "0.16.2"
|
||||
@@ -8269,7 +8068,7 @@ checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057"
|
||||
dependencies = [
|
||||
"smawk",
|
||||
"unicode-linebreak",
|
||||
"unicode-width 0.2.2",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8383,12 +8182,6 @@ dependencies = [
|
||||
"crunchy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiny_pretty"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "650d82e943da333637be9f1567d33d605e76810a26464edfd7ae74f7ef181e95"
|
||||
|
||||
[[package]]
|
||||
name = "tinystr"
|
||||
version = "0.8.1"
|
||||
@@ -8422,7 +8215,7 @@ checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"libc",
|
||||
"mio 1.0.4",
|
||||
"mio",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"socket2 0.6.1",
|
||||
@@ -8992,12 +8785,6 @@ version = "1.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.1.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.2.2"
|
||||
@@ -9775,15 +9562,6 @@ dependencies = [
|
||||
"windows-targets 0.42.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
|
||||
dependencies = [
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
@@ -10299,7 +10077,7 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yaak-app-client"
|
||||
name = "yaak-app"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"charset",
|
||||
@@ -10311,7 +10089,6 @@ dependencies = [
|
||||
"md5 0.8.0",
|
||||
"mime_guess",
|
||||
"openssl-sys",
|
||||
"pretty_graphql",
|
||||
"r2d2",
|
||||
"r2d2_sqlite",
|
||||
"rand 0.9.1",
|
||||
@@ -10357,30 +10134,13 @@ dependencies = [
|
||||
"yaak-tauri-utils",
|
||||
"yaak-templates",
|
||||
"yaak-tls",
|
||||
"yaak-window",
|
||||
"yaak-ws",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yaak-app-proxy"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"log 0.4.29",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-os",
|
||||
"yaak-mac-window",
|
||||
"yaak-proxy-lib",
|
||||
"yaak-rpc",
|
||||
"yaak-window",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yaak-cli"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"arboard",
|
||||
"assert_cmd",
|
||||
"base64 0.22.1",
|
||||
"clap",
|
||||
@@ -10390,7 +10150,6 @@ dependencies = [
|
||||
"futures",
|
||||
"hex",
|
||||
"include_dir",
|
||||
"inquire",
|
||||
"keyring",
|
||||
"log 0.4.29",
|
||||
"oxc_resolver",
|
||||
@@ -10407,7 +10166,6 @@ dependencies = [
|
||||
"walkdir",
|
||||
"webbrowser",
|
||||
"yaak",
|
||||
"yaak-api",
|
||||
"yaak-crypto",
|
||||
"yaak-http",
|
||||
"yaak-models",
|
||||
@@ -10445,25 +10203,6 @@ dependencies = [
|
||||
"yaak-models",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yaak-database"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"include_dir",
|
||||
"log 0.4.29",
|
||||
"nanoid",
|
||||
"r2d2",
|
||||
"r2d2_sqlite",
|
||||
"rusqlite",
|
||||
"sea-query",
|
||||
"sea-query-rusqlite",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.17",
|
||||
"ts-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yaak-fonts"
|
||||
version = "0.1.0"
|
||||
@@ -10536,7 +10275,6 @@ dependencies = [
|
||||
"hyper-util",
|
||||
"log 0.4.29",
|
||||
"mime_guess",
|
||||
"native-tls",
|
||||
"regex 1.11.1",
|
||||
"reqwest",
|
||||
"serde",
|
||||
@@ -10549,7 +10287,6 @@ dependencies = [
|
||||
"urlencoding",
|
||||
"yaak-common",
|
||||
"yaak-models",
|
||||
"yaak-templates",
|
||||
"yaak-tls",
|
||||
"zstd",
|
||||
]
|
||||
@@ -10606,7 +10343,6 @@ dependencies = [
|
||||
"thiserror 2.0.17",
|
||||
"ts-rs",
|
||||
"yaak-core",
|
||||
"yaak-database",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -10623,6 +10359,7 @@ dependencies = [
|
||||
"md5 0.7.0",
|
||||
"path-slash",
|
||||
"rand 0.9.1",
|
||||
"regex 1.11.1",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -10638,52 +10375,6 @@ dependencies = [
|
||||
"zip-extract",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yaak-proxy"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"pem",
|
||||
"rcgen",
|
||||
"rustls",
|
||||
"rustls-native-certs",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yaak-proxy-lib"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"include_dir",
|
||||
"log 0.4.29",
|
||||
"r2d2",
|
||||
"r2d2_sqlite",
|
||||
"rusqlite",
|
||||
"sea-query",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"ts-rs",
|
||||
"yaak-database",
|
||||
"yaak-proxy",
|
||||
"yaak-rpc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yaak-rpc"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"log 0.4.29",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"ts-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yaak-sse"
|
||||
version = "0.1.0"
|
||||
@@ -10749,17 +10440,6 @@ dependencies = [
|
||||
"yaak-models",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yaak-window"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"log 0.4.29",
|
||||
"md5 0.8.0",
|
||||
"rand 0.9.1",
|
||||
"tauri",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yaak-ws"
|
||||
version = "0.1.0"
|
||||
@@ -10791,9 +10471,6 @@ name = "yasna"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd"
|
||||
dependencies = [
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
|
||||
65
Cargo.toml
65
Cargo.toml
@@ -1,38 +1,30 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"crates/yaak",
|
||||
# Common/foundation crates
|
||||
"crates/common/yaak-database",
|
||||
"crates/common/yaak-rpc",
|
||||
# Shared crates (no Tauri dependency)
|
||||
"crates/yaak-core",
|
||||
"crates/yaak-common",
|
||||
"crates/yaak-crypto",
|
||||
"crates/yaak-git",
|
||||
"crates/yaak-grpc",
|
||||
"crates/yaak-http",
|
||||
"crates/yaak-models",
|
||||
"crates/yaak-plugins",
|
||||
"crates/yaak-sse",
|
||||
"crates/yaak-sync",
|
||||
"crates/yaak-templates",
|
||||
"crates/yaak-tls",
|
||||
"crates/yaak-ws",
|
||||
"crates/yaak-api",
|
||||
"crates/yaak-proxy",
|
||||
# Proxy-specific crates
|
||||
"crates-proxy/yaak-proxy-lib",
|
||||
# CLI crates
|
||||
"crates-cli/yaak-cli",
|
||||
# Tauri-specific crates
|
||||
"crates-tauri/yaak-app-client",
|
||||
"crates-tauri/yaak-app-proxy",
|
||||
"crates-tauri/yaak-fonts",
|
||||
"crates-tauri/yaak-license",
|
||||
"crates-tauri/yaak-mac-window",
|
||||
"crates-tauri/yaak-tauri-utils",
|
||||
"crates-tauri/yaak-window",
|
||||
"crates/yaak",
|
||||
# Shared crates (no Tauri dependency)
|
||||
"crates/yaak-core",
|
||||
"crates/yaak-common",
|
||||
"crates/yaak-crypto",
|
||||
"crates/yaak-git",
|
||||
"crates/yaak-grpc",
|
||||
"crates/yaak-http",
|
||||
"crates/yaak-models",
|
||||
"crates/yaak-plugins",
|
||||
"crates/yaak-sse",
|
||||
"crates/yaak-sync",
|
||||
"crates/yaak-templates",
|
||||
"crates/yaak-tls",
|
||||
"crates/yaak-ws",
|
||||
"crates/yaak-api",
|
||||
# CLI crates
|
||||
"crates-cli/yaak-cli",
|
||||
# Tauri-specific crates
|
||||
"crates-tauri/yaak-app",
|
||||
"crates-tauri/yaak-fonts",
|
||||
"crates-tauri/yaak-license",
|
||||
"crates-tauri/yaak-mac-window",
|
||||
"crates-tauri/yaak-tauri-utils",
|
||||
]
|
||||
|
||||
[workspace.dependencies]
|
||||
@@ -55,10 +47,6 @@ thiserror = "2.0.17"
|
||||
tokio = "1.48.0"
|
||||
ts-rs = "11.1.0"
|
||||
|
||||
# Internal crates - common/foundation
|
||||
yaak-database = { path = "crates/common/yaak-database" }
|
||||
yaak-rpc = { path = "crates/common/yaak-rpc" }
|
||||
|
||||
# Internal crates - shared
|
||||
yaak-core = { path = "crates/yaak-core" }
|
||||
yaak = { path = "crates/yaak" }
|
||||
@@ -75,17 +63,12 @@ yaak-templates = { path = "crates/yaak-templates" }
|
||||
yaak-tls = { path = "crates/yaak-tls" }
|
||||
yaak-ws = { path = "crates/yaak-ws" }
|
||||
yaak-api = { path = "crates/yaak-api" }
|
||||
yaak-proxy = { path = "crates/yaak-proxy" }
|
||||
|
||||
# Internal crates - proxy
|
||||
yaak-proxy-lib = { path = "crates-proxy/yaak-proxy-lib" }
|
||||
|
||||
# Internal crates - Tauri-specific
|
||||
yaak-fonts = { path = "crates-tauri/yaak-fonts" }
|
||||
yaak-license = { path = "crates-tauri/yaak-license" }
|
||||
yaak-mac-window = { path = "crates-tauri/yaak-mac-window" }
|
||||
yaak-tauri-utils = { path = "crates-tauri/yaak-tauri-utils" }
|
||||
yaak-window = { path = "crates-tauri/yaak-window" }
|
||||
|
||||
[profile.release]
|
||||
strip = false
|
||||
|
||||
@@ -1,26 +1,24 @@
|
||||
# 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
|
||||
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.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Make sure you have the following tools installed:
|
||||
|
||||
- [Node.js](https://nodejs.org/en/download/package-manager) (v24+)
|
||||
- [Node.js](https://nodejs.org/en/download/package-manager)
|
||||
- [Rust](https://www.rust-lang.org/tools/install)
|
||||
- [Vite+](https://vite.dev/guide/vite-plus) (`vp` CLI)
|
||||
|
||||
Check the installations with the following commands:
|
||||
|
||||
```shell
|
||||
node -v
|
||||
npm -v
|
||||
vp --version
|
||||
rustc --version
|
||||
```
|
||||
|
||||
@@ -47,12 +45,12 @@ npm start
|
||||
## SQLite Migrations
|
||||
|
||||
New migrations can be created from the `src-tauri/` directory:
|
||||
|
||||
|
||||
```shell
|
||||
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._
|
||||
|
||||
@@ -63,9 +61,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
|
||||
```
|
||||
|
||||
## Linting and Formatting
|
||||
## Linting & Formatting
|
||||
|
||||
This repo uses [Vite+](https://vite.dev/guide/vite-plus) for linting (oxlint) and formatting (oxfmt).
|
||||
This repo uses Biome for linting and formatting (replacing ESLint + Prettier).
|
||||
|
||||
- Lint the entire repo:
|
||||
|
||||
@@ -73,6 +71,12 @@ This repo uses [Vite+](https://vite.dev/guide/vite-plus) for linting (oxlint) an
|
||||
npm run lint
|
||||
```
|
||||
|
||||
- Auto-fix lint issues where possible:
|
||||
|
||||
```sh
|
||||
npm run lint:fix
|
||||
```
|
||||
|
||||
- Format code:
|
||||
|
||||
```sh
|
||||
@@ -80,7 +84,5 @@ npm run format
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
16
README.md
16
README.md
@@ -1,6 +1,6 @@
|
||||
<p align="center">
|
||||
<a href="https://github.com/JamesIves/github-sponsors-readme-action">
|
||||
<img width="200px" src="https://github.com/mountain-loop/yaak/raw/main/crates-tauri/yaak-app-client/icons/icon.png">
|
||||
<img width="200px" src="https://github.com/mountain-loop/yaak/raw/main/crates-tauri/yaak-app/icons/icon.png">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
</p>
|
||||
<br>
|
||||
|
||||
|
||||
|
||||
<p align="center">
|
||||
<!-- sponsors-premium --><a href="https://github.com/MVST-Solutions"><img src="https://github.com/MVST-Solutions.png" width="80px" alt="User avatar: MVST-Solutions" /></a> <a href="https://github.com/dharsanb"><img src="https://github.com/dharsanb.png" width="80px" alt="User avatar: dharsanb" /></a> <a href="https://github.com/railwayapp"><img src="https://github.com/railwayapp.png" width="80px" alt="User avatar: railwayapp" /></a> <a href="https://github.com/caseyamcl"><img src="https://github.com/caseyamcl.png" width="80px" alt="User avatar: caseyamcl" /></a> <a href="https://github.com/bytebase"><img src="https://github.com/bytebase.png" width="80px" alt="User avatar: bytebase" /></a> <a href="https://github.com/"><img src="https://raw.githubusercontent.com/JamesIves/github-sponsors-readme-action/dev/.github/assets/placeholder.png" width="80px" alt="User avatar: " /></a> <!-- sponsors-premium -->
|
||||
</p>
|
||||
@@ -25,10 +27,12 @@
|
||||
|
||||

|
||||
|
||||
|
||||
## 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.
|
||||
Built with [Tauri](https://tauri.app), Rust, and React, it’s fast, lightweight, and private. No telemetry, no VC funding, and no cloud lock-in.
|
||||
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, it’s fast, lightweight, and private. No telemetry, no VC funding, and no cloud lock-in.
|
||||
|
||||
|
||||
### 🌐 Work with any API
|
||||
|
||||
@@ -37,23 +41,21 @@ Built with [Tauri](https://tauri.app), Rust, and React, it’s fast, lightweight
|
||||
- Filter and inspect responses with JSONPath or XPath.
|
||||
|
||||
### 🔐 Stay secure
|
||||
|
||||
- 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.
|
||||
|
||||
### ☁️ Organize & collaborate
|
||||
|
||||
- Group requests into workspaces and nested folders.
|
||||
- Use environment variables to switch between dev, staging, and prod.
|
||||
- Mirror workspaces to your filesystem for versioning in Git or syncing with Dropbox.
|
||||
|
||||
### 🧩 Extend & customize
|
||||
|
||||
- Insert dynamic values like UUIDs or timestamps with template tags.
|
||||
- Pick from built-in themes or build your own.
|
||||
- Create plugins to extend authentication, template tags, or the UI.
|
||||
|
||||
|
||||
## Contribution Policy
|
||||
|
||||
> [!IMPORTANT]
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import { getModel } from "@yaakapp-internal/models";
|
||||
import type { FolderSettingsTab } from "../components/FolderSettingsDialog";
|
||||
import { FolderSettingsDialog } from "../components/FolderSettingsDialog";
|
||||
import { showDialog } from "../lib/dialog";
|
||||
|
||||
export function openFolderSettings(folderId: string, tab?: FolderSettingsTab) {
|
||||
const folder = getModel("folder", folderId);
|
||||
if (folder == null) return;
|
||||
showDialog({
|
||||
id: "folder-settings",
|
||||
title: null,
|
||||
size: "lg",
|
||||
className: "h-[50rem]",
|
||||
noPadding: true,
|
||||
render: () => <FolderSettingsDialog folderId={folderId} tab={tab} />,
|
||||
});
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import { applySync, calculateSyncFsOnly } from "@yaakapp-internal/sync";
|
||||
import { createFastMutation } from "../hooks/useFastMutation";
|
||||
import { showSimpleAlert } from "../lib/alert";
|
||||
import { router } from "../lib/router";
|
||||
|
||||
export const openWorkspaceFromSyncDir = createFastMutation<void, void, string>({
|
||||
mutationKey: [],
|
||||
mutationFn: async (dir) => {
|
||||
const ops = await calculateSyncFsOnly(dir);
|
||||
|
||||
const workspace = ops
|
||||
.map((o) => (o.type === "dbCreate" && o.fs.model.type === "workspace" ? o.fs.model : null))
|
||||
.filter((m) => m)[0];
|
||||
|
||||
if (workspace == null) {
|
||||
showSimpleAlert("Failed to Open", "No workspace found in directory");
|
||||
return;
|
||||
}
|
||||
|
||||
await applySync(workspace.id, dir, ops);
|
||||
|
||||
await router.navigate({
|
||||
to: "/workspaces/$workspaceId",
|
||||
params: { workspaceId: workspace.id },
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -1,19 +0,0 @@
|
||||
import type { WorkspaceSettingsTab } from "../components/WorkspaceSettingsDialog";
|
||||
import { WorkspaceSettingsDialog } from "../components/WorkspaceSettingsDialog";
|
||||
import { activeWorkspaceIdAtom } from "../hooks/useActiveWorkspace";
|
||||
import { showDialog } from "../lib/dialog";
|
||||
import { jotaiStore } from "../lib/jotai";
|
||||
|
||||
export function openWorkspaceSettings(tab?: WorkspaceSettingsTab) {
|
||||
const workspaceId = jotaiStore.get(activeWorkspaceIdAtom);
|
||||
if (workspaceId == null) return;
|
||||
showDialog({
|
||||
id: "workspace-settings",
|
||||
size: "md",
|
||||
className: "h-[calc(100vh-5rem)] !max-h-[40rem]",
|
||||
noPadding: true,
|
||||
render: ({ hide }) => (
|
||||
<WorkspaceSettingsDialog workspaceId={workspaceId} hide={hide} tab={tab} />
|
||||
),
|
||||
});
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import { useTimedBoolean } from "@yaakapp-internal/ui";
|
||||
import { copyToClipboard } from "../lib/copy";
|
||||
import { showToast } from "../lib/toast";
|
||||
import type { ButtonProps } from "./core/Button";
|
||||
import { Button } from "./core/Button";
|
||||
|
||||
interface Props extends Omit<ButtonProps, "onClick"> {
|
||||
text: string | (() => Promise<string | null>);
|
||||
}
|
||||
|
||||
export function CopyButton({ text, ...props }: Props) {
|
||||
const [copied, setCopied] = useTimedBoolean();
|
||||
return (
|
||||
<Button
|
||||
{...props}
|
||||
onClick={async () => {
|
||||
const content = typeof text === "function" ? await text() : text;
|
||||
if (content == null) {
|
||||
showToast({
|
||||
id: "failed-to-copy",
|
||||
color: "danger",
|
||||
message: "Failed to copy",
|
||||
});
|
||||
} else {
|
||||
copyToClipboard(content, { disableToast: true });
|
||||
setCopied();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{copied ? "Copied" : "Copy"}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import { IconButton, type IconButtonProps, useTimedBoolean } from "@yaakapp-internal/ui";
|
||||
import { copyToClipboard } from "../lib/copy";
|
||||
import { showToast } from "../lib/toast";
|
||||
|
||||
interface Props extends Omit<IconButtonProps, "onClick" | "icon"> {
|
||||
text: string | (() => Promise<string | null>);
|
||||
}
|
||||
|
||||
export function CopyIconButton({ text, ...props }: Props) {
|
||||
const [copied, setCopied] = useTimedBoolean();
|
||||
return (
|
||||
<IconButton
|
||||
{...props}
|
||||
icon={copied ? "check" : "copy"}
|
||||
showConfirm
|
||||
onClick={async () => {
|
||||
const content = typeof text === "function" ? await text() : text;
|
||||
if (content == null) {
|
||||
showToast({
|
||||
id: "failed-to-copy",
|
||||
color: "danger",
|
||||
message: "Failed to copy",
|
||||
});
|
||||
} else {
|
||||
copyToClipboard(content, { disableToast: true });
|
||||
setCopied();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import classNames from "classnames";
|
||||
import type { CSSProperties } from "react";
|
||||
import { memo } from "react";
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
orientation?: "horizontal" | "vertical";
|
||||
}
|
||||
|
||||
export const DropMarker = memo(
|
||||
function DropMarker({ className, style, orientation = "horizontal" }: Props) {
|
||||
return (
|
||||
<div
|
||||
style={style}
|
||||
className={classNames(
|
||||
className,
|
||||
"absolute pointer-events-none z-50",
|
||||
orientation === "horizontal" && "w-full",
|
||||
orientation === "vertical" && "w-0 top-0 bottom-0",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
"absolute bg-primary rounded-full",
|
||||
orientation === "horizontal" && "left-2 right-2 -bottom-[0.1rem] h-[0.2rem]",
|
||||
orientation === "vertical" && "-left-[0.1rem] top-0 bottom-0 w-[0.2rem]",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
() => true,
|
||||
);
|
||||
@@ -1,38 +0,0 @@
|
||||
import { activeRequestAtom } from "../hooks/useActiveRequest";
|
||||
import { useSubscribeActiveWorkspaceId } from "../hooks/useActiveWorkspace";
|
||||
import { useActiveWorkspaceChangedToast } from "../hooks/useActiveWorkspaceChangedToast";
|
||||
import { useHotKey, useSubscribeHotKeys } from "../hooks/useHotKey";
|
||||
import { useSubscribeHttpAuthentication } from "../hooks/useHttpAuthentication";
|
||||
import { useSyncFontSizeSetting } from "../hooks/useSyncFontSizeSetting";
|
||||
import { useSyncWorkspaceChildModels } from "../hooks/useSyncWorkspaceChildModels";
|
||||
import { useSyncZoomSetting } from "../hooks/useSyncZoomSetting";
|
||||
import { useSubscribeTemplateFunctions } from "../hooks/useTemplateFunctions";
|
||||
import { jotaiStore } from "../lib/jotai";
|
||||
import { renameModelWithPrompt } from "../lib/renameModelWithPrompt";
|
||||
|
||||
export function GlobalHooks() {
|
||||
useSyncZoomSetting();
|
||||
useSyncFontSizeSetting();
|
||||
|
||||
useSubscribeActiveWorkspaceId();
|
||||
|
||||
useSyncWorkspaceChildModels();
|
||||
useSubscribeTemplateFunctions();
|
||||
useSubscribeHttpAuthentication();
|
||||
|
||||
// Other useful things
|
||||
useActiveWorkspaceChangedToast();
|
||||
useSubscribeHotKeys();
|
||||
|
||||
useHotKey(
|
||||
"request.rename",
|
||||
async () => {
|
||||
const model = jotaiStore.get(activeRequestAtom);
|
||||
if (model == null) return;
|
||||
await renameModelWithPrompt(model);
|
||||
},
|
||||
{ allowDefault: true },
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -1,427 +0,0 @@
|
||||
import type { HttpResponse, HttpResponseEvent } from "@yaakapp-internal/models";
|
||||
import { Banner, HStack, Icon, LoadingIcon, VStack } from "@yaakapp-internal/ui";
|
||||
import classNames from "classnames";
|
||||
import type { ComponentType, CSSProperties } from "react";
|
||||
import { lazy, Suspense, useMemo } from "react";
|
||||
import { useCancelHttpResponse } from "../hooks/useCancelHttpResponse";
|
||||
import { useHttpResponseEvents } from "../hooks/useHttpResponseEvents";
|
||||
import { usePinnedHttpResponse } from "../hooks/usePinnedHttpResponse";
|
||||
import { useResponseBodyBytes, useResponseBodyText } from "../hooks/useResponseBodyText";
|
||||
import { useResponseViewMode } from "../hooks/useResponseViewMode";
|
||||
import { useTimelineViewMode } from "../hooks/useTimelineViewMode";
|
||||
import { getMimeTypeFromContentType } from "../lib/contentType";
|
||||
import { getContentTypeFromHeaders, getCookieCounts } from "../lib/model_util";
|
||||
import { ConfirmLargeResponse } from "./ConfirmLargeResponse";
|
||||
import { ConfirmLargeResponseRequest } from "./ConfirmLargeResponseRequest";
|
||||
import { Button } from "./core/Button";
|
||||
import { CountBadge } from "./core/CountBadge";
|
||||
import { HotkeyList } from "./core/HotkeyList";
|
||||
import { HttpResponseDurationTag } from "./core/HttpResponseDurationTag";
|
||||
import { HttpStatusTag } from "./core/HttpStatusTag";
|
||||
import { PillButton } from "./core/PillButton";
|
||||
import { SizeTag } from "./core/SizeTag";
|
||||
import type { TabItem } from "./core/Tabs/Tabs";
|
||||
import { TabContent, Tabs } from "./core/Tabs/Tabs";
|
||||
import { Tooltip } from "./core/Tooltip";
|
||||
import { EmptyStateText } from "./EmptyStateText";
|
||||
import { ErrorBoundary } from "./ErrorBoundary";
|
||||
import { HttpResponseTimeline } from "./HttpResponseTimeline";
|
||||
import { RecentHttpResponsesDropdown } from "./RecentHttpResponsesDropdown";
|
||||
import { RequestBodyViewer } from "./RequestBodyViewer";
|
||||
import { ResponseCookies } from "./ResponseCookies";
|
||||
import { ResponseHeaders } from "./ResponseHeaders";
|
||||
import { AudioViewer } from "./responseViewers/AudioViewer";
|
||||
import { CsvViewer } from "./responseViewers/CsvViewer";
|
||||
import { EventStreamViewer } from "./responseViewers/EventStreamViewer";
|
||||
import { HTMLOrTextViewer } from "./responseViewers/HTMLOrTextViewer";
|
||||
import { ImageViewer } from "./responseViewers/ImageViewer";
|
||||
import { MultipartViewer } from "./responseViewers/MultipartViewer";
|
||||
import { SvgViewer } from "./responseViewers/SvgViewer";
|
||||
import { VideoViewer } from "./responseViewers/VideoViewer";
|
||||
|
||||
const PdfViewer = lazy(() =>
|
||||
import("./responseViewers/PdfViewer").then((m) => ({ default: m.PdfViewer })),
|
||||
);
|
||||
|
||||
interface Props {
|
||||
style?: CSSProperties;
|
||||
className?: string;
|
||||
activeRequestId: string;
|
||||
}
|
||||
|
||||
const TAB_BODY = "body";
|
||||
const TAB_REQUEST = "request";
|
||||
const TAB_HEADERS = "headers";
|
||||
const TAB_COOKIES = "cookies";
|
||||
const TAB_TIMELINE = "timeline";
|
||||
|
||||
export type TimelineViewMode = "timeline" | "text";
|
||||
|
||||
interface RedirectDropWarning {
|
||||
droppedBodyCount: number;
|
||||
droppedHeaders: string[];
|
||||
}
|
||||
|
||||
export function HttpResponsePane({ style, className, activeRequestId }: Props) {
|
||||
const { activeResponse, setPinnedResponseId, responses } = usePinnedHttpResponse(activeRequestId);
|
||||
const [viewMode, setViewMode] = useResponseViewMode(activeResponse?.requestId);
|
||||
const [timelineViewMode, setTimelineViewMode] = useTimelineViewMode();
|
||||
const contentType = getContentTypeFromHeaders(activeResponse?.headers ?? null);
|
||||
const mimeType = contentType == null ? null : getMimeTypeFromContentType(contentType).essence;
|
||||
|
||||
const responseEvents = useHttpResponseEvents(activeResponse);
|
||||
const redirectDropWarning = useMemo(
|
||||
() => getRedirectDropWarning(responseEvents.data),
|
||||
[responseEvents.data],
|
||||
);
|
||||
const shouldShowRedirectDropWarning =
|
||||
activeResponse?.state === "closed" && redirectDropWarning != null;
|
||||
|
||||
const cookieCounts = useMemo(() => getCookieCounts(responseEvents.data), [responseEvents.data]);
|
||||
|
||||
const tabs = useMemo<TabItem[]>(
|
||||
() => [
|
||||
{
|
||||
value: TAB_BODY,
|
||||
label: "Response",
|
||||
options: {
|
||||
value: viewMode,
|
||||
onChange: setViewMode,
|
||||
items: [
|
||||
{ label: "Response", value: "pretty" },
|
||||
...(mimeType?.startsWith("image")
|
||||
? []
|
||||
: [{ label: "Response (Raw)", shortLabel: "Raw", value: "raw" }]),
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
value: TAB_REQUEST,
|
||||
label: "Request",
|
||||
rightSlot:
|
||||
(activeResponse?.requestContentLength ?? 0) > 0 ? <CountBadge count={true} /> : null,
|
||||
},
|
||||
{
|
||||
value: TAB_HEADERS,
|
||||
label: "Headers",
|
||||
rightSlot: (
|
||||
<CountBadge
|
||||
count={activeResponse?.requestHeaders.length ?? 0}
|
||||
count2={activeResponse?.headers.length ?? 0}
|
||||
showZero
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
value: TAB_COOKIES,
|
||||
label: "Cookies",
|
||||
rightSlot:
|
||||
cookieCounts.sent > 0 || cookieCounts.received > 0 ? (
|
||||
<CountBadge count={cookieCounts.sent} count2={cookieCounts.received} showZero />
|
||||
) : null,
|
||||
},
|
||||
{
|
||||
value: TAB_TIMELINE,
|
||||
rightSlot: <CountBadge count={responseEvents.data?.length ?? 0} />,
|
||||
options: {
|
||||
value: timelineViewMode,
|
||||
onChange: (v) => setTimelineViewMode((v as TimelineViewMode) ?? "timeline"),
|
||||
items: [
|
||||
{ label: "Timeline", value: "timeline" },
|
||||
{ label: "Timeline (Text)", shortLabel: "Timeline", value: "text" },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
activeResponse?.headers,
|
||||
activeResponse?.requestContentLength,
|
||||
activeResponse?.requestHeaders.length,
|
||||
cookieCounts.sent,
|
||||
cookieCounts.received,
|
||||
mimeType,
|
||||
responseEvents.data?.length,
|
||||
setViewMode,
|
||||
viewMode,
|
||||
timelineViewMode,
|
||||
setTimelineViewMode,
|
||||
],
|
||||
);
|
||||
|
||||
const cancel = useCancelHttpResponse(activeResponse?.id ?? null);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={style}
|
||||
className={classNames(
|
||||
className,
|
||||
"x-theme-responsePane",
|
||||
"max-h-full h-full",
|
||||
"bg-surface rounded-md border border-border-subtle overflow-hidden",
|
||||
"relative",
|
||||
)}
|
||||
>
|
||||
{activeResponse == null ? (
|
||||
<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">
|
||||
<HStack
|
||||
className={classNames(
|
||||
"text-text-subtle w-full flex-shrink-0",
|
||||
// Remove a bit of space because the tabs have lots too
|
||||
"-mb-1.5",
|
||||
)}
|
||||
>
|
||||
{activeResponse && (
|
||||
<div
|
||||
className={classNames(
|
||||
"grid grid-cols-[auto_minmax(4rem,1fr)_auto]",
|
||||
"cursor-default select-none",
|
||||
"whitespace-nowrap w-full pl-3 overflow-x-auto font-mono text-sm hide-scrollbars",
|
||||
)}
|
||||
>
|
||||
<HStack space={2} className="w-full flex-shrink-0">
|
||||
{activeResponse.state !== "closed" && <LoadingIcon size="sm" />}
|
||||
<HttpStatusTag showReason response={activeResponse} />
|
||||
<span>•</span>
|
||||
<HttpResponseDurationTag response={activeResponse} />
|
||||
<span>•</span>
|
||||
<SizeTag
|
||||
contentLength={activeResponse.contentLength ?? 0}
|
||||
contentLengthCompressed={activeResponse.contentLengthCompressed}
|
||||
/>
|
||||
</HStack>
|
||||
{shouldShowRedirectDropWarning ? (
|
||||
<Tooltip
|
||||
tabIndex={0}
|
||||
className="my-auto pl-3 flex-shrink-0 max-w-full justify-self-end overflow-hidden"
|
||||
content={
|
||||
<VStack alignItems="start" space={1} className="text-xs">
|
||||
<span className="font-medium text-warning">
|
||||
Redirect changed this request
|
||||
</span>
|
||||
{redirectDropWarning.droppedBodyCount > 0 && (
|
||||
<span>
|
||||
Body dropped on {redirectDropWarning.droppedBodyCount}{" "}
|
||||
{redirectDropWarning.droppedBodyCount === 1
|
||||
? "redirect hop"
|
||||
: "redirect hops"}
|
||||
</span>
|
||||
)}
|
||||
{redirectDropWarning.droppedHeaders.length > 0 && (
|
||||
<span>
|
||||
Headers dropped:{" "}
|
||||
<span className="font-mono">
|
||||
{redirectDropWarning.droppedHeaders.join(", ")}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
<span className="text-text-subtle">See Timeline for details.</span>
|
||||
</VStack>
|
||||
}
|
||||
>
|
||||
<span className="inline-flex min-w-0">
|
||||
<PillButton
|
||||
color="warning"
|
||||
className="font-sans text-sm !flex-shrink max-w-full"
|
||||
innerClassName="flex items-center"
|
||||
leftSlot={<Icon icon="alert_triangle" size="xs" color="warning" />}
|
||||
>
|
||||
<span className="truncate">
|
||||
{getRedirectWarningLabel(redirectDropWarning)}
|
||||
</span>
|
||||
</PillButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<span />
|
||||
)}
|
||||
<div className="justify-self-end flex-shrink-0">
|
||||
<RecentHttpResponsesDropdown
|
||||
responses={responses}
|
||||
activeResponse={activeResponse}
|
||||
onPinnedResponseId={setPinnedResponseId}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</HStack>
|
||||
|
||||
<div className="overflow-hidden flex flex-col min-h-0">
|
||||
{activeResponse?.error && (
|
||||
<Banner color="danger" className="mx-3 mt-1 flex-shrink-0">
|
||||
{activeResponse.error}
|
||||
</Banner>
|
||||
)}
|
||||
{/* Show tabs if we have any data (headers, body, etc.) even if there's an error */}
|
||||
<Tabs
|
||||
tabs={tabs}
|
||||
label="Response"
|
||||
className="ml-3 mr-3 mb-3 min-h-0 flex-1"
|
||||
tabListClassName="mt-0.5 -mb-1.5"
|
||||
storageKey="http_response_tabs"
|
||||
activeTabKey={activeRequestId}
|
||||
>
|
||||
<TabContent value={TAB_BODY}>
|
||||
<ErrorBoundary name="Http Response Viewer">
|
||||
<Suspense>
|
||||
<ConfirmLargeResponse response={activeResponse}>
|
||||
{activeResponse.state === "initialized" ? (
|
||||
<EmptyStateText>
|
||||
<VStack space={3}>
|
||||
<HStack space={3}>
|
||||
<LoadingIcon className="text-text-subtlest" />
|
||||
Sending Request
|
||||
</HStack>
|
||||
<Button size="sm" variant="border" onClick={() => cancel.mutate()}>
|
||||
Cancel
|
||||
</Button>
|
||||
</VStack>
|
||||
</EmptyStateText>
|
||||
) : activeResponse.state === "closed" &&
|
||||
(activeResponse.contentLength ?? 0) === 0 ? (
|
||||
<EmptyStateText>Empty</EmptyStateText>
|
||||
) : mimeType?.match(/^text\/event-stream/i) && viewMode === "pretty" ? (
|
||||
<EventStreamViewer response={activeResponse} />
|
||||
) : mimeType?.match(/^image\/svg/) ? (
|
||||
<HttpSvgViewer response={activeResponse} />
|
||||
) : mimeType?.match(/^image/i) ? (
|
||||
<EnsureCompleteResponse response={activeResponse} Component={ImageViewer} />
|
||||
) : mimeType?.match(/^audio/i) ? (
|
||||
<EnsureCompleteResponse response={activeResponse} Component={AudioViewer} />
|
||||
) : mimeType?.match(/^video/i) ? (
|
||||
<EnsureCompleteResponse response={activeResponse} Component={VideoViewer} />
|
||||
) : mimeType?.match(/^multipart/i) && viewMode === "pretty" ? (
|
||||
<HttpMultipartViewer response={activeResponse} />
|
||||
) : mimeType?.match(/pdf/i) ? (
|
||||
<EnsureCompleteResponse response={activeResponse} Component={PdfViewer} />
|
||||
) : mimeType?.match(/csv|tab-separated/i) && viewMode === "pretty" ? (
|
||||
<HttpCsvViewer className="pb-2" response={activeResponse} />
|
||||
) : (
|
||||
<HTMLOrTextViewer
|
||||
textViewerClassName="-mr-2 bg-surface" // Pull to the right
|
||||
response={activeResponse}
|
||||
pretty={viewMode === "pretty"}
|
||||
/>
|
||||
)}
|
||||
</ConfirmLargeResponse>
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
</TabContent>
|
||||
<TabContent value={TAB_REQUEST}>
|
||||
<ConfirmLargeResponseRequest response={activeResponse}>
|
||||
<RequestBodyViewer response={activeResponse} />
|
||||
</ConfirmLargeResponseRequest>
|
||||
</TabContent>
|
||||
<TabContent value={TAB_HEADERS}>
|
||||
<ResponseHeaders response={activeResponse} />
|
||||
</TabContent>
|
||||
<TabContent value={TAB_COOKIES}>
|
||||
<ResponseCookies response={activeResponse} />
|
||||
</TabContent>
|
||||
<TabContent value={TAB_TIMELINE}>
|
||||
<HttpResponseTimeline response={activeResponse} viewMode={timelineViewMode} />
|
||||
</TabContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function getRedirectDropWarning(
|
||||
events: HttpResponseEvent[] | undefined,
|
||||
): RedirectDropWarning | null {
|
||||
if (events == null || events.length === 0) return null;
|
||||
|
||||
let droppedBodyCount = 0;
|
||||
const droppedHeaders = new Set<string>();
|
||||
for (const e of events) {
|
||||
const event = e.event;
|
||||
if (event.type !== "redirect") {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (event.dropped_body) {
|
||||
droppedBodyCount += 1;
|
||||
}
|
||||
for (const headerName of event.dropped_headers ?? []) {
|
||||
pushHeaderName(droppedHeaders, headerName);
|
||||
}
|
||||
}
|
||||
|
||||
if (droppedBodyCount === 0 && droppedHeaders.size === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
droppedBodyCount,
|
||||
droppedHeaders: Array.from(droppedHeaders).sort(),
|
||||
};
|
||||
}
|
||||
|
||||
function pushHeaderName(headers: Set<string>, headerName: string): void {
|
||||
const existing = Array.from(headers).find((h) => h.toLowerCase() === headerName.toLowerCase());
|
||||
if (existing == null) {
|
||||
headers.add(headerName);
|
||||
}
|
||||
}
|
||||
|
||||
function getRedirectWarningLabel(warning: RedirectDropWarning): string {
|
||||
if (warning.droppedBodyCount > 0 && warning.droppedHeaders.length > 0) {
|
||||
return "Dropped body and headers";
|
||||
}
|
||||
if (warning.droppedBodyCount > 0) {
|
||||
return "Dropped body";
|
||||
}
|
||||
return "Dropped headers";
|
||||
}
|
||||
|
||||
function EnsureCompleteResponse({
|
||||
response,
|
||||
Component,
|
||||
}: {
|
||||
response: HttpResponse;
|
||||
Component: ComponentType<{ bodyPath: string }>;
|
||||
}) {
|
||||
if (response.bodyPath === null) {
|
||||
return <div>Empty response body</div>;
|
||||
}
|
||||
|
||||
// Wait until the response has been fully-downloaded
|
||||
if (response.state !== "closed") {
|
||||
return (
|
||||
<EmptyStateText>
|
||||
<LoadingIcon />
|
||||
</EmptyStateText>
|
||||
);
|
||||
}
|
||||
|
||||
return <Component bodyPath={response.bodyPath} />;
|
||||
}
|
||||
|
||||
function HttpSvgViewer({ response }: { response: HttpResponse }) {
|
||||
const body = useResponseBodyText({ response, filter: null });
|
||||
|
||||
if (!body.data) return null;
|
||||
|
||||
return <SvgViewer text={body.data} />;
|
||||
}
|
||||
|
||||
function HttpCsvViewer({ response, className }: { response: HttpResponse; className?: string }) {
|
||||
const body = useResponseBodyText({ response, filter: null });
|
||||
|
||||
return <CsvViewer text={body.data ?? null} className={className} />;
|
||||
}
|
||||
|
||||
function HttpMultipartViewer({ response }: { response: HttpResponse }) {
|
||||
const body = useResponseBodyBytes({ response });
|
||||
|
||||
if (body.data == null) return null;
|
||||
|
||||
const contentTypeHeader = getContentTypeFromHeaders(response.headers);
|
||||
const boundary = contentTypeHeader?.split("boundary=")[1] ?? "unknown";
|
||||
|
||||
return <MultipartViewer data={body.data} boundary={boundary} idPrefix={response.id} />;
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
import { linter } from "@codemirror/lint";
|
||||
import type { HttpRequest } from "@yaakapp-internal/models";
|
||||
import { patchModel } from "@yaakapp-internal/models";
|
||||
import { Banner, Icon } from "@yaakapp-internal/ui";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useKeyValue } from "../hooks/useKeyValue";
|
||||
import { fireAndForget } from "../lib/fireAndForget";
|
||||
import { textLikelyContainsJsonComments } from "../lib/jsonComments";
|
||||
import type { DropdownItem } from "./core/Dropdown";
|
||||
import { Dropdown } from "./core/Dropdown";
|
||||
import type { EditorProps } from "./core/Editor/Editor";
|
||||
import { jsonParseLinter } from "./core/Editor/json-lint";
|
||||
import { Editor } from "./core/Editor/LazyEditor";
|
||||
import { IconButton } from "./core/IconButton";
|
||||
import { IconTooltip } from "./core/IconTooltip";
|
||||
|
||||
interface Props {
|
||||
forceUpdateKey: string;
|
||||
heightMode: EditorProps["heightMode"];
|
||||
request: HttpRequest;
|
||||
}
|
||||
|
||||
export function JsonBodyEditor({ forceUpdateKey, heightMode, request }: Props) {
|
||||
const handleChange = useCallback(
|
||||
(text: string) => patchModel(request, { body: { ...request.body, text } }),
|
||||
[request],
|
||||
);
|
||||
|
||||
const autoFix = request.body?.sendJsonComments !== true;
|
||||
|
||||
const lintExtension = useMemo(
|
||||
() =>
|
||||
linter(
|
||||
jsonParseLinter(
|
||||
autoFix
|
||||
? { allowComments: true, allowTrailingCommas: true }
|
||||
: { allowComments: false, allowTrailingCommas: false },
|
||||
),
|
||||
),
|
||||
[autoFix],
|
||||
);
|
||||
|
||||
const hasComments = useMemo(
|
||||
() => textLikelyContainsJsonComments(request.body?.text ?? ""),
|
||||
[request.body?.text],
|
||||
);
|
||||
|
||||
const { value: bannerDismissed, set: setBannerDismissed } = useKeyValue<boolean>({
|
||||
namespace: "no_sync",
|
||||
key: ["json-fix-3", request.workspaceId],
|
||||
fallback: false,
|
||||
});
|
||||
|
||||
const handleToggleAutoFix = useCallback(() => {
|
||||
const newBody = { ...request.body };
|
||||
if (autoFix) {
|
||||
newBody.sendJsonComments = true;
|
||||
} else {
|
||||
delete newBody.sendJsonComments;
|
||||
}
|
||||
fireAndForget(patchModel(request, { body: newBody }));
|
||||
}, [request, autoFix]);
|
||||
|
||||
const handleDropdownOpen = useCallback(() => {
|
||||
if (!bannerDismissed) {
|
||||
fireAndForget(setBannerDismissed(true));
|
||||
}
|
||||
}, [bannerDismissed, setBannerDismissed]);
|
||||
|
||||
const showBanner = hasComments && autoFix && !bannerDismissed;
|
||||
|
||||
const stripMessage = "Automatically strip comments and trailing commas before sending";
|
||||
const actions = useMemo<EditorProps["actions"]>(
|
||||
() => [
|
||||
showBanner && (
|
||||
<Banner color="notice" className="!opacity-100 h-sm !py-0 !px-2 flex items-center text-xs">
|
||||
<p className="inline-flex items-center gap-1 min-w-0">
|
||||
<span className="truncate">Auto-fix enabled</span>
|
||||
<Icon icon="arrow_right" size="sm" className="opacity-disabled" />
|
||||
</p>
|
||||
</Banner>
|
||||
),
|
||||
<div key="settings" className="!opacity-100 !shadow">
|
||||
<Dropdown
|
||||
onOpen={handleDropdownOpen}
|
||||
items={
|
||||
[
|
||||
{
|
||||
label: "Automatically Fix JSON",
|
||||
keepOpenOnSelect: true,
|
||||
onSelect: handleToggleAutoFix,
|
||||
rightSlot: <IconTooltip content={stripMessage} />,
|
||||
leftSlot: (
|
||||
<Icon icon={autoFix ? "check_square_checked" : "check_square_unchecked"} />
|
||||
),
|
||||
},
|
||||
] satisfies DropdownItem[]
|
||||
}
|
||||
>
|
||||
<IconButton size="sm" variant="border" icon="settings" title="JSON Settings" />
|
||||
</Dropdown>
|
||||
</div>,
|
||||
],
|
||||
[handleDropdownOpen, handleToggleAutoFix, autoFix, showBanner],
|
||||
);
|
||||
|
||||
return (
|
||||
<Editor
|
||||
forceUpdateKey={forceUpdateKey}
|
||||
autocompleteFunctions
|
||||
autocompleteVariables
|
||||
placeholder="..."
|
||||
heightMode={heightMode}
|
||||
defaultValue={`${request.body?.text ?? ""}`}
|
||||
language="json"
|
||||
onChange={handleChange}
|
||||
stateKey={`json.${request.id}`}
|
||||
actions={actions}
|
||||
lintExtension={lintExtension}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
import type { CSSProperties } from "react";
|
||||
import ReactMarkdown, { type Components } from "react-markdown";
|
||||
import { PrismLight as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import { ErrorBoundary } from "./ErrorBoundary";
|
||||
import { Prose } from "./Prose";
|
||||
|
||||
interface Props {
|
||||
children: string | null;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function Markdown({ children, className }: Props) {
|
||||
if (children == null) return null;
|
||||
|
||||
return (
|
||||
<Prose className={className}>
|
||||
<ErrorBoundary name="Markdown">
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
|
||||
{children}
|
||||
</ReactMarkdown>
|
||||
</ErrorBoundary>
|
||||
</Prose>
|
||||
);
|
||||
}
|
||||
|
||||
const prismTheme = {
|
||||
'pre[class*="language-"]': {
|
||||
// Needs to be here, so the lib doesn't add its own
|
||||
},
|
||||
|
||||
// Syntax tokens
|
||||
comment: { color: "var(--textSubtle)" },
|
||||
prolog: { color: "var(--textSubtle)" },
|
||||
doctype: { color: "var(--textSubtle)" },
|
||||
cdata: { color: "var(--textSubtle)" },
|
||||
|
||||
punctuation: { color: "var(--textSubtle)" },
|
||||
|
||||
property: { color: "var(--primary)" },
|
||||
"attr-name": { color: "var(--primary)" },
|
||||
|
||||
string: { color: "var(--notice)" },
|
||||
char: { color: "var(--notice)" },
|
||||
|
||||
number: { color: "var(--info)" },
|
||||
constant: { color: "var(--info)" },
|
||||
symbol: { color: "var(--info)" },
|
||||
|
||||
boolean: { color: "var(--warning)" },
|
||||
"attr-value": { color: "var(--warning)" },
|
||||
|
||||
variable: { color: "var(--success)" },
|
||||
|
||||
tag: { color: "var(--info)" },
|
||||
operator: { color: "var(--danger)" },
|
||||
keyword: { color: "var(--danger)" },
|
||||
function: { color: "var(--success)" },
|
||||
"class-name": { color: "var(--primary)" },
|
||||
builtin: { color: "var(--danger)" },
|
||||
selector: { color: "var(--danger)" },
|
||||
inserted: { color: "var(--success)" },
|
||||
deleted: { color: "var(--danger)" },
|
||||
regex: { color: "var(--warning)" },
|
||||
|
||||
important: { color: "var(--danger)", fontWeight: "bold" },
|
||||
italic: { fontStyle: "italic" },
|
||||
bold: { fontWeight: "bold" },
|
||||
entity: { cursor: "help" },
|
||||
};
|
||||
|
||||
const lineStyle: CSSProperties = {
|
||||
paddingRight: "1.5em",
|
||||
paddingLeft: "0",
|
||||
opacity: 0.5,
|
||||
};
|
||||
|
||||
const markdownComponents: Partial<Components> = {
|
||||
// Ensure links open in external browser by adding target="_blank"
|
||||
a: ({ href, children, ...rest }) => {
|
||||
if (href && !href.match(/https?:\/\//)) {
|
||||
href = `http://${href}`;
|
||||
}
|
||||
return (
|
||||
<a target="_blank" rel="noreferrer noopener" href={href} {...rest}>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
},
|
||||
code(props) {
|
||||
const { children, className, ref, ...extraProps } = props;
|
||||
extraProps.node = undefined;
|
||||
|
||||
const match = /language-(\w+)/.exec(className || "");
|
||||
return match ? (
|
||||
<SyntaxHighlighter
|
||||
{...extraProps}
|
||||
CodeTag="code"
|
||||
showLineNumbers
|
||||
PreTag="div"
|
||||
lineNumberStyle={lineStyle}
|
||||
language={match[1]}
|
||||
style={prismTheme}
|
||||
>
|
||||
{String(children as string).replace(/\n$/, "")}
|
||||
</SyntaxHighlighter>
|
||||
) : (
|
||||
<code {...extraProps} ref={ref} className={className}>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
},
|
||||
};
|
||||
@@ -1,12 +0,0 @@
|
||||
import classNames from "classnames";
|
||||
import type { ReactNode } from "react";
|
||||
import "./Prose.css";
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function Prose({ className, ...props }: Props) {
|
||||
return <div className={classNames("prose", className)} {...props} />;
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
import { workspacesAtom } from "@yaakapp-internal/models";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { useEffect } from "react";
|
||||
import { getRecentCookieJars } from "../hooks/useRecentCookieJars";
|
||||
import { getRecentEnvironments } from "../hooks/useRecentEnvironments";
|
||||
import { getRecentRequests } from "../hooks/useRecentRequests";
|
||||
import { useRecentWorkspaces } from "../hooks/useRecentWorkspaces";
|
||||
import { fireAndForget } from "../lib/fireAndForget";
|
||||
import { router } from "../lib/router";
|
||||
|
||||
export function RedirectToLatestWorkspace() {
|
||||
const workspaces = useAtomValue(workspacesAtom);
|
||||
const recentWorkspaces = useRecentWorkspaces();
|
||||
|
||||
useEffect(() => {
|
||||
if (workspaces.length === 0 || recentWorkspaces == null) {
|
||||
console.log("No workspaces found to redirect to. Skipping.", {
|
||||
workspaces,
|
||||
recentWorkspaces,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
fireAndForget(
|
||||
(async () => {
|
||||
const workspaceId = recentWorkspaces[0] ?? workspaces[0]?.id ?? "n/a";
|
||||
const environmentId = (await getRecentEnvironments(workspaceId))[0] ?? null;
|
||||
const cookieJarId = (await getRecentCookieJars(workspaceId))[0] ?? null;
|
||||
const requestId = (await getRecentRequests(workspaceId))[0] ?? null;
|
||||
const params = { workspaceId };
|
||||
const search = {
|
||||
cookie_jar_id: cookieJarId,
|
||||
environment_id: environmentId,
|
||||
request_id: requestId,
|
||||
};
|
||||
|
||||
console.log("Redirecting to workspace", params, search);
|
||||
await router.navigate({ to: "/workspaces/$workspaceId", params, search });
|
||||
})(),
|
||||
);
|
||||
}, [recentWorkspaces, workspaces, workspaces.length]);
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
import { openUrl } from "@tauri-apps/plugin-opener";
|
||||
import { useLicense } from "@yaakapp-internal/license";
|
||||
import { useRef } from "react";
|
||||
import { openSettings } from "../commands/openSettings";
|
||||
import { useCheckForUpdates } from "../hooks/useCheckForUpdates";
|
||||
import { useExportData } from "../hooks/useExportData";
|
||||
import { appInfo } from "../lib/appInfo";
|
||||
import { showDialog } from "../lib/dialog";
|
||||
import { importData } from "../lib/importData";
|
||||
import type { DropdownRef } from "./core/Dropdown";
|
||||
import { Dropdown } from "./core/Dropdown";
|
||||
import { Icon } from "@yaakapp-internal/ui";
|
||||
import { IconButton } from "./core/IconButton";
|
||||
import { KeyboardShortcutsDialog } from "./KeyboardShortcutsDialog";
|
||||
|
||||
export function SettingsDropdown() {
|
||||
const exportData = useExportData();
|
||||
const dropdownRef = useRef<DropdownRef>(null);
|
||||
const checkForUpdates = useCheckForUpdates();
|
||||
const { check } = useLicense();
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
ref={dropdownRef}
|
||||
items={[
|
||||
{
|
||||
label: "Settings",
|
||||
hotKeyAction: "settings.show",
|
||||
leftSlot: <Icon icon="settings" />,
|
||||
onSelect: () => openSettings.mutate(null),
|
||||
},
|
||||
{
|
||||
label: "Keyboard shortcuts",
|
||||
hotKeyAction: "hotkeys.showHelp",
|
||||
leftSlot: <Icon icon="keyboard" />,
|
||||
onSelect: () => {
|
||||
showDialog({
|
||||
id: "hotkey",
|
||||
title: "Keyboard Shortcuts",
|
||||
size: "dynamic",
|
||||
render: () => <KeyboardShortcutsDialog />,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Plugins",
|
||||
leftSlot: <Icon icon="puzzle" />,
|
||||
onSelect: () => openSettings.mutate("plugins"),
|
||||
},
|
||||
{ type: "separator", label: "Share Workspace(s)" },
|
||||
{
|
||||
label: "Import Data",
|
||||
leftSlot: <Icon icon="folder_input" />,
|
||||
onSelect: () => importData.mutate(),
|
||||
},
|
||||
{
|
||||
label: "Export Data",
|
||||
leftSlot: <Icon icon="folder_output" />,
|
||||
onSelect: () => exportData.mutate(),
|
||||
},
|
||||
{
|
||||
label: "Create Run Button",
|
||||
leftSlot: <Icon icon="rocket" />,
|
||||
onSelect: () => openUrl("https://yaak.app/button/new"),
|
||||
},
|
||||
{ type: "separator", label: `Yaak v${appInfo.version}` },
|
||||
{
|
||||
label: "Check for Updates",
|
||||
leftSlot: <Icon icon="update" />,
|
||||
hidden: !appInfo.featureUpdater,
|
||||
onSelect: () => checkForUpdates.mutate(),
|
||||
},
|
||||
{
|
||||
label: "Purchase License",
|
||||
color: "success",
|
||||
hidden: check.data == null || check.data.status === "active",
|
||||
leftSlot: <Icon icon="circle_dollar_sign" />,
|
||||
rightSlot: <Icon icon="external_link" color="success" className="opacity-60" />,
|
||||
onSelect: () => openUrl("https://yaak.app/pricing"),
|
||||
},
|
||||
{
|
||||
label: "Install CLI",
|
||||
hidden: appInfo.cliVersion != null,
|
||||
leftSlot: <Icon icon="square_terminal" />,
|
||||
rightSlot: <Icon icon="external_link" color="secondary" />,
|
||||
onSelect: () => openUrl("https://yaak.app/docs/cli"),
|
||||
},
|
||||
{
|
||||
label: "Feedback",
|
||||
leftSlot: <Icon icon="chat" />,
|
||||
rightSlot: <Icon icon="external_link" color="secondary" />,
|
||||
onSelect: () => openUrl("https://yaak.app/feedback"),
|
||||
},
|
||||
{
|
||||
label: "Changelog",
|
||||
leftSlot: <Icon icon="cake" />,
|
||||
rightSlot: <Icon icon="external_link" color="secondary" />,
|
||||
onSelect: () => openUrl(`https://yaak.app/changelog/${appInfo.version}`),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<IconButton
|
||||
size="sm"
|
||||
title="Main Menu"
|
||||
icon="settings"
|
||||
iconColor="secondary"
|
||||
className="pointer-events-auto"
|
||||
/>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import { HStack } from "@yaakapp-internal/ui";
|
||||
import { useMemo } from "react";
|
||||
import { useFloatingSidebarHidden } from "../hooks/useFloatingSidebarHidden";
|
||||
import { useSidebarHidden } from "../hooks/useSidebarHidden";
|
||||
import { CreateDropdown } from "./CreateDropdown";
|
||||
import { IconButton } from "./core/IconButton";
|
||||
|
||||
interface Props {
|
||||
floating?: boolean;
|
||||
}
|
||||
|
||||
export function SidebarActions({ floating = false }: Props) {
|
||||
const [sidebarHidden, setSidebarHidden] = useSidebarHidden();
|
||||
const [floatingHidden, setFloatingHidden] = useFloatingSidebarHidden();
|
||||
|
||||
const hidden = floating ? floatingHidden : sidebarHidden;
|
||||
const setHidden = useMemo(
|
||||
() => (floating ? setFloatingHidden : setSidebarHidden),
|
||||
[floating, setFloatingHidden, setSidebarHidden],
|
||||
);
|
||||
|
||||
return (
|
||||
<HStack className="h-full">
|
||||
<IconButton
|
||||
onClick={async () => {
|
||||
await setHidden(!hidden);
|
||||
}}
|
||||
className="pointer-events-auto"
|
||||
size="sm"
|
||||
title="Toggle sidebar"
|
||||
icon={hidden ? "left_panel_hidden" : "left_panel_visible"}
|
||||
iconColor="secondary"
|
||||
/>
|
||||
<CreateDropdown hotKeyAction="model.create">
|
||||
<IconButton size="sm" icon="plus_circle" iconColor="secondary" title="Add Resource" />
|
||||
</CreateDropdown>
|
||||
</HStack>
|
||||
);
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
import type { WebsocketRequest } from "@yaakapp-internal/models";
|
||||
import classNames from "classnames";
|
||||
import { useAtomValue } from "jotai";
|
||||
import type { CSSProperties } from "react";
|
||||
|
||||
import { SplitLayout } from "@yaakapp-internal/ui";
|
||||
import { activeWorkspaceAtom } from "../hooks/useActiveWorkspace";
|
||||
import { workspaceLayoutAtom } from "../lib/atoms";
|
||||
import { WebsocketRequestPane } from "./WebsocketRequestPane";
|
||||
import { WebsocketResponsePane } from "./WebsocketResponsePane";
|
||||
|
||||
interface Props {
|
||||
activeRequest: WebsocketRequest;
|
||||
style: CSSProperties;
|
||||
}
|
||||
|
||||
export function WebsocketRequestLayout({ activeRequest, style }: Props) {
|
||||
const workspaceLayout = useAtomValue(workspaceLayoutAtom);
|
||||
const activeWorkspace = useAtomValue(activeWorkspaceAtom);
|
||||
const wsId = activeWorkspace?.id ?? "n/a";
|
||||
return (
|
||||
<SplitLayout
|
||||
storageKey={`websocket_layout::${wsId}`}
|
||||
className="p-3 gap-1.5"
|
||||
layout={workspaceLayout}
|
||||
style={style}
|
||||
firstSlot={({ orientation, style }) => (
|
||||
<WebsocketRequestPane
|
||||
style={style}
|
||||
activeRequest={activeRequest}
|
||||
fullHeight={orientation === "horizontal"}
|
||||
/>
|
||||
)}
|
||||
secondSlot={({ style }) => (
|
||||
<div
|
||||
style={style}
|
||||
className={classNames(
|
||||
"x-theme-responsePane",
|
||||
"max-h-full h-full grid grid-rows-[minmax(0,1fr)] grid-cols-1",
|
||||
"bg-surface rounded-md border border-border-subtle",
|
||||
"shadow relative",
|
||||
)}
|
||||
>
|
||||
<WebsocketResponsePane activeRequest={activeRequest} />
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,221 +0,0 @@
|
||||
import { type } from "@tauri-apps/plugin-os";
|
||||
import { settingsAtom, workspacesAtom } from "@yaakapp-internal/models";
|
||||
import { Banner, HeaderSize, HStack, SidebarLayout } from "@yaakapp-internal/ui";
|
||||
import classNames from "classnames";
|
||||
import { useAtomValue } from "jotai";
|
||||
import * as m from "motion/react-m";
|
||||
import { useMemo, useState } from "react";
|
||||
import {
|
||||
useEnsureActiveCookieJar,
|
||||
useSubscribeActiveCookieJarId,
|
||||
} from "../hooks/useActiveCookieJar";
|
||||
import {
|
||||
activeEnvironmentAtom,
|
||||
useSubscribeActiveEnvironmentId,
|
||||
} from "../hooks/useActiveEnvironment";
|
||||
import { activeFolderAtom } from "../hooks/useActiveFolder";
|
||||
import { useSubscribeActiveFolderId } from "../hooks/useActiveFolderId";
|
||||
import { activeRequestAtom } from "../hooks/useActiveRequest";
|
||||
import { useSubscribeActiveRequestId } from "../hooks/useActiveRequestId";
|
||||
import { activeWorkspaceAtom } from "../hooks/useActiveWorkspace";
|
||||
import { useFloatingSidebarHidden } from "../hooks/useFloatingSidebarHidden";
|
||||
import { useHotKey } from "../hooks/useHotKey";
|
||||
import { useSubscribeRecentCookieJars } from "../hooks/useRecentCookieJars";
|
||||
import { useSubscribeRecentEnvironments } from "../hooks/useRecentEnvironments";
|
||||
import { useSubscribeRecentRequests } from "../hooks/useRecentRequests";
|
||||
import { useSubscribeRecentWorkspaces } from "../hooks/useRecentWorkspaces";
|
||||
import { useSidebarHidden } from "../hooks/useSidebarHidden";
|
||||
import { useSidebarWidth } from "../hooks/useSidebarWidth";
|
||||
import { useSyncWorkspaceRequestTitle } from "../hooks/useSyncWorkspaceRequestTitle";
|
||||
import { duplicateRequestOrFolderAndNavigate } from "../lib/duplicateRequestOrFolderAndNavigate";
|
||||
import { importData } from "../lib/importData";
|
||||
import { jotaiStore } from "../lib/jotai";
|
||||
import { CreateDropdown } from "./CreateDropdown";
|
||||
import { Button } from "./core/Button";
|
||||
import { HotkeyList } from "./core/HotkeyList";
|
||||
import { FeedbackLink } from "./core/Link";
|
||||
import { ErrorBoundary } from "./ErrorBoundary";
|
||||
import { FolderLayout } from "./FolderLayout";
|
||||
import { GrpcConnectionLayout } from "./GrpcConnectionLayout";
|
||||
import { HttpRequestLayout } from "./HttpRequestLayout";
|
||||
import Sidebar from "./Sidebar";
|
||||
import { SidebarActions } from "./SidebarActions";
|
||||
import { WebsocketRequestLayout } from "./WebsocketRequestLayout";
|
||||
import { WorkspaceHeader } from "./WorkspaceHeader";
|
||||
|
||||
const body = { gridArea: "body" };
|
||||
|
||||
export function Workspace() {
|
||||
// First, subscribe to some things applicable to workspaces
|
||||
useGlobalWorkspaceHooks();
|
||||
|
||||
const workspaces = useAtomValue(workspacesAtom);
|
||||
const settings = useAtomValue(settingsAtom);
|
||||
const osType = type();
|
||||
const [width, setWidth] = useSidebarWidth();
|
||||
const [sidebarHidden, setSidebarHidden] = useSidebarHidden();
|
||||
const [floatingSidebarHidden, setFloatingSidebarHidden] = useFloatingSidebarHidden();
|
||||
const activeEnvironment = useAtomValue(activeEnvironmentAtom);
|
||||
const [floating, setFloating] = useState(false);
|
||||
|
||||
const environmentBgStyle = useMemo(() => {
|
||||
if (activeEnvironment?.color == null) return undefined;
|
||||
const background = `linear-gradient(to right, ${activeEnvironment.color} 15%, transparent 40%)`;
|
||||
return { background };
|
||||
}, [activeEnvironment?.color]);
|
||||
|
||||
// We're loading still
|
||||
if (workspaces.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const header = (
|
||||
<HeaderSize
|
||||
data-tauri-drag-region
|
||||
size="lg"
|
||||
className="relative x-theme-appHeader bg-surface"
|
||||
osType={osType}
|
||||
hideWindowControls={settings.hideWindowControls}
|
||||
useNativeTitlebar={settings.useNativeTitlebar}
|
||||
interfaceScale={settings.interfaceScale}
|
||||
>
|
||||
<div className="absolute inset-0 pointer-events-none">
|
||||
<div style={environmentBgStyle} className="absolute inset-0 opacity-[0.07]" />
|
||||
<div
|
||||
style={environmentBgStyle}
|
||||
className="absolute left-0 right-0 -bottom-[1px] h-[1px] opacity-20"
|
||||
/>
|
||||
</div>
|
||||
<WorkspaceHeader className="pointer-events-none" floatingSidebar={floating} />
|
||||
</HeaderSize>
|
||||
);
|
||||
|
||||
const workspaceBody = (
|
||||
<ErrorBoundary name="Workspace Body">
|
||||
<WorkspaceBody />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
||||
const sidebarContent = floating ? (
|
||||
<div
|
||||
className={classNames(
|
||||
"x-theme-sidebar",
|
||||
"h-full bg-surface border-r border-border-subtle",
|
||||
"grid grid-rows-[auto_1fr]",
|
||||
)}
|
||||
>
|
||||
<HeaderSize
|
||||
hideControls
|
||||
size="lg"
|
||||
className="border-transparent flex items-center"
|
||||
osType={osType}
|
||||
hideWindowControls={settings.hideWindowControls}
|
||||
useNativeTitlebar={settings.useNativeTitlebar}
|
||||
interfaceScale={settings.interfaceScale}
|
||||
>
|
||||
<SidebarActions floating />
|
||||
</HeaderSize>
|
||||
<ErrorBoundary name="Sidebar (Floating)">
|
||||
<Sidebar />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
) : (
|
||||
<div className="x-theme-sidebar overflow-hidden bg-surface h-full">
|
||||
<ErrorBoundary name="Sidebar">
|
||||
<Sidebar className="border-r border-border-subtle" />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="grid w-full h-full grid-rows-[auto_1fr]">
|
||||
{header}
|
||||
<SidebarLayout
|
||||
width={width ?? 250}
|
||||
onWidthChange={setWidth}
|
||||
hidden={sidebarHidden ?? false}
|
||||
onHiddenChange={(hidden) => setSidebarHidden(hidden)}
|
||||
floatingHidden={floatingSidebarHidden ?? true}
|
||||
onFloatingHiddenChange={(hidden) => setFloatingSidebarHidden(hidden)}
|
||||
onFloatingChange={setFloating}
|
||||
sidebar={sidebarContent}
|
||||
>
|
||||
{workspaceBody}
|
||||
</SidebarLayout>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function WorkspaceBody() {
|
||||
const activeRequest = useAtomValue(activeRequestAtom);
|
||||
const activeFolder = useAtomValue(activeFolderAtom);
|
||||
const activeWorkspace = useAtomValue(activeWorkspaceAtom);
|
||||
|
||||
if (activeWorkspace == null) {
|
||||
return (
|
||||
<m.div
|
||||
className="m-auto"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
// Delay the entering because the workspaces might load after a slight delay
|
||||
transition={{ delay: 0.5 }}
|
||||
>
|
||||
<Banner color="warning" className="max-w-[30rem]">
|
||||
The active workspace was not found. Select a workspace from the header menu or report this
|
||||
bug to <FeedbackLink />
|
||||
</Banner>
|
||||
</m.div>
|
||||
);
|
||||
}
|
||||
|
||||
if (activeRequest?.model === "grpc_request") {
|
||||
return <GrpcConnectionLayout style={body} />;
|
||||
}
|
||||
if (activeRequest?.model === "websocket_request") {
|
||||
return <WebsocketRequestLayout style={body} activeRequest={activeRequest} />;
|
||||
}
|
||||
if (activeRequest?.model === "http_request") {
|
||||
return <HttpRequestLayout activeRequest={activeRequest} style={body} />;
|
||||
}
|
||||
if (activeFolder != null) {
|
||||
return <FolderLayout folder={activeFolder} style={body} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<HotkeyList
|
||||
hotkeys={["model.create", "sidebar.focus", "settings.show"]}
|
||||
bottomSlot={
|
||||
<HStack space={1} justifyContent="center" className="mt-3">
|
||||
<Button variant="border" size="sm" onClick={() => importData.mutate()}>
|
||||
Import
|
||||
</Button>
|
||||
<CreateDropdown hideFolder>
|
||||
<Button variant="border" forDropdown size="sm">
|
||||
New Request
|
||||
</Button>
|
||||
</CreateDropdown>
|
||||
</HStack>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function useGlobalWorkspaceHooks() {
|
||||
useEnsureActiveCookieJar();
|
||||
|
||||
useSubscribeActiveRequestId();
|
||||
useSubscribeActiveFolderId();
|
||||
useSubscribeActiveEnvironmentId();
|
||||
useSubscribeActiveCookieJarId();
|
||||
|
||||
useSubscribeRecentRequests();
|
||||
useSubscribeRecentWorkspaces();
|
||||
useSubscribeRecentEnvironments();
|
||||
useSubscribeRecentCookieJars();
|
||||
|
||||
useSyncWorkspaceRequestTitle();
|
||||
|
||||
useHotKey("model.duplicate", () =>
|
||||
duplicateRequestOrFolderAndNavigate(jotaiStore.get(activeRequestAtom)),
|
||||
);
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
import { HStack, Icon } from "@yaakapp-internal/ui";
|
||||
import classNames from "classnames";
|
||||
import { useAtom, useAtomValue } from "jotai";
|
||||
import { memo } from "react";
|
||||
import { activeWorkspaceAtom, activeWorkspaceMetaAtom } from "../hooks/useActiveWorkspace";
|
||||
import { useToggleCommandPalette } from "../hooks/useToggleCommandPalette";
|
||||
import { workspaceLayoutAtom } from "../lib/atoms";
|
||||
import { setupOrConfigureEncryption } from "../lib/setupOrConfigureEncryption";
|
||||
import { CookieDropdown } from "./CookieDropdown";
|
||||
import { IconButton } from "./core/IconButton";
|
||||
import { PillButton } from "./core/PillButton";
|
||||
import { EnvironmentActionsDropdown } from "./EnvironmentActionsDropdown";
|
||||
import { ImportCurlButton } from "./ImportCurlButton";
|
||||
import { LicenseBadge } from "./LicenseBadge";
|
||||
import { RecentRequestsDropdown } from "./RecentRequestsDropdown";
|
||||
import { SettingsDropdown } from "./SettingsDropdown";
|
||||
import { SidebarActions } from "./SidebarActions";
|
||||
import { WorkspaceActionsDropdown } from "./WorkspaceActionsDropdown";
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
floatingSidebar?: boolean;
|
||||
}
|
||||
|
||||
export const WorkspaceHeader = memo(function WorkspaceHeader({
|
||||
className,
|
||||
floatingSidebar,
|
||||
}: Props) {
|
||||
const togglePalette = useToggleCommandPalette();
|
||||
const [workspaceLayout, setWorkspaceLayout] = useAtom(workspaceLayoutAtom);
|
||||
const workspace = useAtomValue(activeWorkspaceAtom);
|
||||
const workspaceMeta = useAtomValue(activeWorkspaceMetaAtom);
|
||||
const showEncryptionSetup =
|
||||
workspace != null &&
|
||||
workspaceMeta != null &&
|
||||
workspace.encryptionKeyChallenge != null &&
|
||||
workspaceMeta.encryptionKey == null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
className,
|
||||
"grid grid-cols-[auto_minmax(0,1fr)_auto] items-center w-full h-full",
|
||||
)}
|
||||
>
|
||||
<HStack space={0.5} className={classNames("flex-1 pointer-events-none")}>
|
||||
<SidebarActions floating={floatingSidebar} />
|
||||
<CookieDropdown />
|
||||
<HStack className="min-w-0">
|
||||
<WorkspaceActionsDropdown />
|
||||
<Icon icon="chevron_right" color="secondary" />
|
||||
<EnvironmentActionsDropdown className="w-auto pointer-events-auto" />
|
||||
</HStack>
|
||||
</HStack>
|
||||
<div className="pointer-events-none w-full max-w-[30vw] mx-auto flex justify-center">
|
||||
<RecentRequestsDropdown />
|
||||
</div>
|
||||
<div className="flex-1 flex gap-1 items-center h-full justify-end pointer-events-none pr-1">
|
||||
<ImportCurlButton />
|
||||
{showEncryptionSetup ? (
|
||||
<PillButton color="danger" onClick={setupOrConfigureEncryption}>
|
||||
Enter Encryption Key
|
||||
</PillButton>
|
||||
) : (
|
||||
<LicenseBadge />
|
||||
)}
|
||||
<IconButton
|
||||
icon={
|
||||
workspaceLayout === "responsive"
|
||||
? "magic_wand"
|
||||
: workspaceLayout === "horizontal"
|
||||
? "columns_2"
|
||||
: "rows_2"
|
||||
}
|
||||
title={`Change to ${workspaceLayout === "horizontal" ? "vertical" : "horizontal"} layout`}
|
||||
size="sm"
|
||||
iconColor="secondary"
|
||||
onClick={() =>
|
||||
setWorkspaceLayout((prev) => (prev === "horizontal" ? "vertical" : "horizontal"))
|
||||
}
|
||||
/>
|
||||
<IconButton
|
||||
icon="search"
|
||||
title="Search or execute a command"
|
||||
size="sm"
|
||||
hotkeyAction="command_palette.toggle"
|
||||
iconColor="secondary"
|
||||
onClick={togglePalette}
|
||||
/>
|
||||
<SettingsDropdown />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -1,34 +0,0 @@
|
||||
import { Button as BaseButton, type ButtonProps as BaseButtonProps } from "@yaakapp-internal/ui";
|
||||
import { forwardRef, useImperativeHandle, useRef } from "react";
|
||||
import type { HotkeyAction } from "../../hooks/useHotKey";
|
||||
import { useFormattedHotkey, useHotKey } from "../../hooks/useHotKey";
|
||||
|
||||
export type ButtonProps = BaseButtonProps & {
|
||||
hotkeyAction?: HotkeyAction;
|
||||
hotkeyLabelOnly?: boolean;
|
||||
hotkeyPriority?: number;
|
||||
};
|
||||
|
||||
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
|
||||
{ hotkeyAction, hotkeyPriority, hotkeyLabelOnly, title, ...props }: ButtonProps,
|
||||
ref,
|
||||
) {
|
||||
const hotkeyTrigger = useFormattedHotkey(hotkeyAction ?? null)?.join("");
|
||||
const fullTitle = hotkeyTrigger ? `${title ?? ""} ${hotkeyTrigger}`.trim() : title;
|
||||
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
useImperativeHandle<HTMLButtonElement | null, HTMLButtonElement | null>(
|
||||
ref,
|
||||
() => buttonRef.current,
|
||||
);
|
||||
|
||||
useHotKey(
|
||||
hotkeyAction ?? null,
|
||||
() => {
|
||||
buttonRef.current?.click();
|
||||
},
|
||||
{ priority: hotkeyPriority, enable: !hotkeyLabelOnly },
|
||||
);
|
||||
|
||||
return <BaseButton ref={buttonRef} title={fullTitle} {...props} />;
|
||||
});
|
||||
@@ -1,12 +0,0 @@
|
||||
import { lazy, Suspense } from "react";
|
||||
import type { EditorProps } from "./Editor";
|
||||
|
||||
const Editor_ = lazy(() => import("./Editor").then((m) => ({ default: m.Editor })));
|
||||
|
||||
export function Editor(props: EditorProps) {
|
||||
return (
|
||||
<Suspense>
|
||||
<Editor_ {...props} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
// This file was generated by lezer-generator. You probably shouldn't edit it.
|
||||
import { LRParser } from "@lezer/lr";
|
||||
import { highlight } from "./highlight";
|
||||
export const parser = LRParser.deserialize({
|
||||
version: 14,
|
||||
states:
|
||||
"!pQQOPOOO`OPO'#C^OeOPO'#CaOjOPO'#CcOoOPO'#CeOOOO'#Ci'#CiOOOO'#Cg'#CgQQOPOOOOOO,58x,58xOOOO,58{,58{OOOO,58},58}OOOO,59P,59POOOO-E6e-E6e",
|
||||
stateData: "z~ORPOUQOWROYSO~OSWO~OSXO~OSYO~OSZO~ORUWYW~",
|
||||
goto: "m^PP_PP_P_P_PcPiTTOVQVOR[VTUOV",
|
||||
nodeNames:
|
||||
"⚠ Timeline OutgoingLine OutgoingText Newline IncomingLine IncomingText InfoLine InfoText PlainLine PlainText",
|
||||
maxTerm: 13,
|
||||
propSources: [highlight],
|
||||
skippedNodes: [0],
|
||||
repeatNodeCount: 1,
|
||||
tokenData:
|
||||
"%h~RZOYtYZ!]Zztz{!b{!^t!^!_#d!_!`t!`!a$f!a;'St;'S;=`!V<%lOt~ySY~OYtZ;'St;'S;=`!V<%lOt~!YP;=`<%lt~!bOS~~!gUY~OYtZptpq!yq;'St;'S;=`!V<%lOt~#QSW~Y~OY!yZ;'S!y;'S;=`#^<%lO!y~#aP;=`<%l!y~#iUY~OYtZptpq#{q;'St;'S;=`!V<%lOt~$SSU~Y~OY#{Z;'S#{;'S;=`$`<%lO#{~$cP;=`<%l#{~$kUY~OYtZptpq$}q;'St;'S;=`!V<%lOt~%USR~Y~OY$}Z;'S$};'S;=`%b<%lO$}~%eP;=`<%l$}",
|
||||
tokenizers: [0],
|
||||
topRules: { Timeline: [0, 1] },
|
||||
tokenPrec: 36,
|
||||
});
|
||||
@@ -1,108 +0,0 @@
|
||||
/* oxlint-disable no-template-curly-in-string */
|
||||
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { parser } from "./twig";
|
||||
|
||||
function getNodeNames(input: string): string[] {
|
||||
const tree = parser.parse(input);
|
||||
const nodes: string[] = [];
|
||||
const cursor = tree.cursor();
|
||||
do {
|
||||
if (cursor.name !== "Template") {
|
||||
nodes.push(cursor.name);
|
||||
}
|
||||
} while (cursor.next());
|
||||
return nodes;
|
||||
}
|
||||
|
||||
function hasTag(input: string): boolean {
|
||||
return getNodeNames(input).includes("Tag");
|
||||
}
|
||||
|
||||
function hasError(input: string): boolean {
|
||||
return getNodeNames(input).includes("⚠");
|
||||
}
|
||||
|
||||
describe("twig grammar", () => {
|
||||
describe("${[var]} format (valid template tags)", () => {
|
||||
test("parses simple variable as Tag", () => {
|
||||
expect(hasTag("${[var]}")).toBe(true);
|
||||
expect(hasError("${[var]}")).toBe(false);
|
||||
});
|
||||
|
||||
test("parses variable with whitespace as Tag", () => {
|
||||
expect(hasTag("${[ var ]}")).toBe(true);
|
||||
expect(hasError("${[ var ]}")).toBe(false);
|
||||
});
|
||||
|
||||
test("parses embedded variable as Tag", () => {
|
||||
expect(hasTag("hello ${[name]} world")).toBe(true);
|
||||
expect(hasError("hello ${[name]} world")).toBe(false);
|
||||
});
|
||||
|
||||
test("parses function call as Tag", () => {
|
||||
expect(hasTag("${[fn()]}")).toBe(true);
|
||||
expect(hasError("${[fn()]}")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("${var} format (should be plain text, not tags)", () => {
|
||||
test("parses ${var} as plain Text without errors", () => {
|
||||
expect(hasTag("${var}")).toBe(false);
|
||||
expect(hasError("${var}")).toBe(false);
|
||||
});
|
||||
|
||||
test("parses embedded ${var} as plain Text", () => {
|
||||
expect(hasTag("hello ${name} world")).toBe(false);
|
||||
expect(hasError("hello ${name} world")).toBe(false);
|
||||
});
|
||||
|
||||
test("parses JSON with ${var} as plain Text", () => {
|
||||
const json = '{"key": "${value}"}';
|
||||
expect(hasTag(json)).toBe(false);
|
||||
expect(hasError(json)).toBe(false);
|
||||
});
|
||||
|
||||
test("parses multiple ${var} as plain Text", () => {
|
||||
expect(hasTag("${a} and ${b}")).toBe(false);
|
||||
expect(hasError("${a} and ${b}")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("mixed content", () => {
|
||||
test("distinguishes ${var} from ${[var]} in same string", () => {
|
||||
const input = "${plain} and ${[tag]}";
|
||||
expect(hasTag(input)).toBe(true);
|
||||
expect(hasError(input)).toBe(false);
|
||||
});
|
||||
|
||||
test("parses JSON with ${[var]} as having Tag", () => {
|
||||
const json = '{"key": "${[value]}"}';
|
||||
expect(hasTag(json)).toBe(true);
|
||||
expect(hasError(json)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("edge cases", () => {
|
||||
test("handles $ at end of string", () => {
|
||||
expect(hasError("hello$")).toBe(false);
|
||||
expect(hasTag("hello$")).toBe(false);
|
||||
});
|
||||
|
||||
test("handles ${ at end of string without crash", () => {
|
||||
// Incomplete syntax may produce errors, but should not crash
|
||||
expect(() => parser.parse("hello${")).not.toThrow();
|
||||
});
|
||||
|
||||
test("handles ${[ without closing without crash", () => {
|
||||
// Unclosed tag may produce partial match, but should not crash
|
||||
expect(() => parser.parse("${[unclosed")).not.toThrow();
|
||||
});
|
||||
|
||||
test("handles empty ${[]}", () => {
|
||||
// Empty tags may or may not be valid depending on grammar
|
||||
// Just ensure no crash
|
||||
expect(() => parser.parse("${[]}")).not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,9 +0,0 @@
|
||||
import { genericCompletion } from "../genericCompletion";
|
||||
|
||||
export const completions = genericCompletion({
|
||||
options: [
|
||||
{ label: "http://", type: "constant" },
|
||||
{ label: "https://", type: "constant" },
|
||||
],
|
||||
minMatch: 1,
|
||||
});
|
||||
@@ -1,15 +0,0 @@
|
||||
import classNames from "classnames";
|
||||
import type { HotkeyAction } from "../../hooks/useHotKey";
|
||||
import { useHotkeyLabel } from "../../hooks/useHotKey";
|
||||
|
||||
interface Props {
|
||||
action: HotkeyAction;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function HotkeyLabel({ action, className }: Props) {
|
||||
const label = useHotkeyLabel(action);
|
||||
return (
|
||||
<span className={classNames(className, "text-text-subtle whitespace-nowrap")}>{label}</span>
|
||||
);
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
import type { GrpcRequest, HttpRequest, WebsocketRequest } from "@yaakapp-internal/models";
|
||||
import { settingsAtom } from "@yaakapp-internal/models";
|
||||
import classNames from "classnames";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { memo } from "react";
|
||||
|
||||
interface Props {
|
||||
request: HttpRequest | GrpcRequest | WebsocketRequest;
|
||||
className?: string;
|
||||
short?: boolean;
|
||||
noAlias?: boolean;
|
||||
}
|
||||
|
||||
const methodNames: Record<string, string> = {
|
||||
get: "GET",
|
||||
put: "PUT",
|
||||
post: "POST",
|
||||
patch: "PTCH",
|
||||
delete: "DELE",
|
||||
options: "OPTN",
|
||||
head: "HEAD",
|
||||
query: "QURY",
|
||||
graphql: "GQL",
|
||||
grpc: "GRPC",
|
||||
websocket: "WS",
|
||||
};
|
||||
|
||||
export const HttpMethodTag = memo(function HttpMethodTag({
|
||||
request,
|
||||
className,
|
||||
short,
|
||||
noAlias,
|
||||
}: Props) {
|
||||
const method =
|
||||
request.model === "http_request" && request.bodyType === "graphql" && !noAlias
|
||||
? "graphql"
|
||||
: request.model === "grpc_request"
|
||||
? "grpc"
|
||||
: request.model === "websocket_request"
|
||||
? "websocket"
|
||||
: request.method;
|
||||
|
||||
return <HttpMethodTagRaw method={method} className={className} short={short} />;
|
||||
});
|
||||
|
||||
export function HttpMethodTagRaw({
|
||||
className,
|
||||
method,
|
||||
short,
|
||||
forceColor,
|
||||
}: {
|
||||
method: string;
|
||||
className?: string;
|
||||
short?: boolean;
|
||||
forceColor?: boolean;
|
||||
}) {
|
||||
let label = method.toUpperCase();
|
||||
if (short) {
|
||||
label = methodNames[method.toLowerCase()] ?? method.slice(0, 4);
|
||||
label = label.padStart(4, " ");
|
||||
}
|
||||
|
||||
const m = method.toUpperCase();
|
||||
|
||||
const settings = useAtomValue(settingsAtom);
|
||||
const colored = forceColor || settings.coloredMethods;
|
||||
|
||||
return (
|
||||
<span
|
||||
className={classNames(
|
||||
className,
|
||||
!colored && "text-text-subtle",
|
||||
colored && m === "GRAPHQL" && "text-info",
|
||||
colored && m === "WEBSOCKET" && "text-info",
|
||||
colored && m === "GRPC" && "text-info",
|
||||
colored && m === "QUERY" && "text-text-subtle",
|
||||
colored && m === "OPTIONS" && "text-info",
|
||||
colored && m === "HEAD" && "text-text-subtle",
|
||||
colored && m === "GET" && "text-primary",
|
||||
colored && m === "PUT" && "text-warning",
|
||||
colored && m === "PATCH" && "text-notice",
|
||||
colored && m === "POST" && "text-success",
|
||||
colored && m === "DELETE" && "text-danger",
|
||||
"font-mono flex-shrink-0 whitespace-pre",
|
||||
"pt-[0.15em]", // Fix for monospace font not vertically centering
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
import {
|
||||
IconButton as BaseIconButton,
|
||||
type IconButtonProps as BaseIconButtonProps,
|
||||
} from "@yaakapp-internal/ui";
|
||||
import { forwardRef, useImperativeHandle, useRef } from "react";
|
||||
import type { HotkeyAction } from "../../hooks/useHotKey";
|
||||
import { useFormattedHotkey, useHotKey } from "../../hooks/useHotKey";
|
||||
|
||||
export type IconButtonProps = BaseIconButtonProps & {
|
||||
hotkeyAction?: HotkeyAction;
|
||||
hotkeyLabelOnly?: boolean;
|
||||
hotkeyPriority?: number;
|
||||
};
|
||||
|
||||
export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(function IconButton(
|
||||
{ hotkeyAction, hotkeyPriority, hotkeyLabelOnly, title, ...props }: IconButtonProps,
|
||||
ref,
|
||||
) {
|
||||
const hotkeyTrigger = useFormattedHotkey(hotkeyAction ?? null)?.join("");
|
||||
const fullTitle = hotkeyTrigger ? `${title ?? ""} ${hotkeyTrigger}`.trim() : title;
|
||||
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
useImperativeHandle<HTMLButtonElement | null, HTMLButtonElement | null>(
|
||||
ref,
|
||||
() => buttonRef.current,
|
||||
);
|
||||
|
||||
useHotKey(
|
||||
hotkeyAction ?? null,
|
||||
() => {
|
||||
buttonRef.current?.click();
|
||||
},
|
||||
{ priority: hotkeyPriority, enable: !hotkeyLabelOnly },
|
||||
);
|
||||
|
||||
return <BaseIconButton ref={buttonRef} title={fullTitle} {...props} />;
|
||||
});
|
||||
@@ -1,9 +0,0 @@
|
||||
import { generateId } from "../../lib/generateId";
|
||||
import type { Pair, PairWithId } from "./PairEditor";
|
||||
|
||||
export function ensurePairId(p: Pair): PairWithId {
|
||||
if (typeof p.id === "string") {
|
||||
return p as PairWithId;
|
||||
}
|
||||
return { ...p, id: p.id ?? generateId() };
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import classNames from "classnames";
|
||||
import type { ButtonProps } from "./Button";
|
||||
import { Button } from "./Button";
|
||||
|
||||
export function PillButton({ className, ...props }: ButtonProps) {
|
||||
return (
|
||||
<Button
|
||||
size="2xs"
|
||||
variant="border"
|
||||
className={classNames(className, "!rounded-full mx-1 !px-3")}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import type { Color } from "@yaakapp-internal/plugins";
|
||||
import classNames from "classnames";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
interface Props {
|
||||
orientation?: "horizontal" | "vertical";
|
||||
dashed?: boolean;
|
||||
className?: string;
|
||||
children?: ReactNode;
|
||||
color?: Color;
|
||||
}
|
||||
|
||||
export function Separator({
|
||||
color,
|
||||
className,
|
||||
dashed,
|
||||
orientation = "horizontal",
|
||||
children,
|
||||
}: Props) {
|
||||
return (
|
||||
<div role="presentation" className={classNames(className, "flex items-center w-full")}>
|
||||
{children && (
|
||||
<div className="text-sm text-text-subtlest mr-2 whitespace-nowrap">{children}</div>
|
||||
)}
|
||||
<div
|
||||
className={classNames(
|
||||
"h-0 border-t opacity-60",
|
||||
color == null && "border-border",
|
||||
color === "primary" && "border-primary",
|
||||
color === "secondary" && "border-secondary",
|
||||
color === "success" && "border-success",
|
||||
color === "notice" && "border-notice",
|
||||
color === "warning" && "border-warning",
|
||||
color === "danger" && "border-danger",
|
||||
color === "info" && "border-info",
|
||||
dashed && "border-dashed",
|
||||
orientation === "horizontal" && "w-full h-[1px]",
|
||||
orientation === "vertical" && "h-full w-[1px]",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import type { WebsocketConnection } from "@yaakapp-internal/models";
|
||||
import classNames from "classnames";
|
||||
|
||||
interface Props {
|
||||
connection: WebsocketConnection;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function WebsocketStatusTag({ connection, className }: Props) {
|
||||
const { state, error } = connection;
|
||||
|
||||
let label: string;
|
||||
let colorClass = "text-text-subtle";
|
||||
|
||||
if (error) {
|
||||
label = "ERROR";
|
||||
colorClass = "text-danger";
|
||||
} else if (state === "connected") {
|
||||
label = "CONNECTED";
|
||||
colorClass = "text-success";
|
||||
} else if (state === "closing") {
|
||||
label = "CLOSING";
|
||||
} else if (state === "closed") {
|
||||
label = "CLOSED";
|
||||
colorClass = "text-warning";
|
||||
} else {
|
||||
label = "CONNECTING";
|
||||
}
|
||||
|
||||
return <span className={classNames(className, "font-mono", colorClass)}>{label}</span>;
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
import { showPromptForm } from "../../lib/prompt-form";
|
||||
import { Banner, InlineCode } from "@yaakapp-internal/ui";
|
||||
|
||||
export interface GitCredentials {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export async function promptCredentials({
|
||||
url: remoteUrl,
|
||||
error,
|
||||
}: {
|
||||
url: string;
|
||||
error: string | null;
|
||||
}): Promise<GitCredentials | null> {
|
||||
const isGitHub = /github\.com/i.test(remoteUrl);
|
||||
const userLabel = isGitHub ? "GitHub Username" : "Username";
|
||||
const passLabel = isGitHub ? "GitHub Personal Access Token" : "Password / Token";
|
||||
const userDescription = isGitHub ? "Use your GitHub username (not your email)." : undefined;
|
||||
const passDescription = isGitHub
|
||||
? "GitHub requires a Personal Access Token (PAT) for write operations over HTTPS. Passwords are not supported."
|
||||
: "Enter your password or access token for this Git server.";
|
||||
const r = await showPromptForm({
|
||||
id: "git-credentials",
|
||||
title: "Credentials Required",
|
||||
description: error ? (
|
||||
<Banner color="danger">{error}</Banner>
|
||||
) : (
|
||||
<>
|
||||
Enter credentials for <InlineCode>{remoteUrl}</InlineCode>
|
||||
</>
|
||||
),
|
||||
inputs: [
|
||||
{ type: "text", name: "username", label: userLabel, description: userDescription },
|
||||
{
|
||||
type: "text",
|
||||
name: "password",
|
||||
label: passLabel,
|
||||
description: passDescription,
|
||||
password: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
if (r == null) return null;
|
||||
|
||||
const username = String(r.username || "");
|
||||
const password = String(r.password || "");
|
||||
return { username, password };
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import type { PullResult, PushResult } from "@yaakapp-internal/git";
|
||||
import { showToast } from "../../lib/toast";
|
||||
|
||||
export function handlePushResult(r: PushResult) {
|
||||
switch (r.type) {
|
||||
case "needs_credentials":
|
||||
showToast({ id: "push-error", message: "Credentials not found", color: "danger" });
|
||||
break;
|
||||
case "success":
|
||||
showToast({ id: "push-success", message: r.message, color: "success" });
|
||||
break;
|
||||
case "up_to_date":
|
||||
showToast({ id: "push-nothing", message: "Already up-to-date", color: "info" });
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
export function handlePullResult(r: PullResult) {
|
||||
switch (r.type) {
|
||||
case "needs_credentials":
|
||||
showToast({ id: "pull-error", message: "Credentials not found", color: "danger" });
|
||||
break;
|
||||
case "success":
|
||||
showToast({ id: "pull-success", message: r.message, color: "success" });
|
||||
break;
|
||||
case "up_to_date":
|
||||
showToast({ id: "pull-nothing", message: "Already up-to-date", color: "info" });
|
||||
break;
|
||||
case "diverged":
|
||||
// Handled by mutation callback before reaching here
|
||||
break;
|
||||
case "uncommitted_changes":
|
||||
// Handled by mutation callback before reaching here
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import type { GitRemote } from "@yaakapp-internal/git";
|
||||
import { gitMutations } from "@yaakapp-internal/git";
|
||||
import { showPromptForm } from "../../lib/prompt-form";
|
||||
import { gitCallbacks } from "./callbacks";
|
||||
|
||||
export async function addGitRemote(dir: string, defaultName?: string): Promise<GitRemote> {
|
||||
const r = await showPromptForm({
|
||||
id: "add-remote",
|
||||
title: "Add Remote",
|
||||
inputs: [
|
||||
{ type: "text", label: "Name", name: "name", defaultValue: defaultName },
|
||||
{ type: "text", label: "URL", name: "url" },
|
||||
],
|
||||
});
|
||||
if (r == null) throw new Error("Cancelled remote prompt");
|
||||
|
||||
const name = String(r.name ?? "");
|
||||
const url = String(r.url ?? "");
|
||||
return gitMutations(dir, gitCallbacks(dir)).addRemote.mutateAsync({ name, url });
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import type { UncommittedChangesStrategy } from "@yaakapp-internal/git";
|
||||
import { showConfirm } from "../../lib/confirm";
|
||||
|
||||
export async function promptUncommittedChangesStrategy(): Promise<UncommittedChangesStrategy> {
|
||||
const confirmed = await showConfirm({
|
||||
id: "git-uncommitted-changes",
|
||||
title: "Uncommitted Changes",
|
||||
description: "You have uncommitted changes. Commit or reset your changes before pulling.",
|
||||
confirmText: "Reset and Pull",
|
||||
color: "danger",
|
||||
});
|
||||
return confirmed ? "reset" : "cancel";
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
// Listen for settings changes, the re-compute theme
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import type { ModelPayload } from "@yaakapp-internal/models";
|
||||
import { fireAndForget } from "./lib/fireAndForget";
|
||||
import { getSettings } from "./lib/settings";
|
||||
|
||||
function setFontSizeOnDocument(fontSize: number) {
|
||||
document.documentElement.style.fontSize = `${fontSize}px`;
|
||||
}
|
||||
|
||||
listen<ModelPayload>("model_write", async (event) => {
|
||||
if (event.payload.change.type !== "upsert") return;
|
||||
if (event.payload.model.model !== "settings") return;
|
||||
setFontSizeOnDocument(event.payload.model.interfaceFontSize);
|
||||
}).catch(console.error);
|
||||
|
||||
fireAndForget(getSettings().then((settings) => setFontSizeOnDocument(settings.interfaceFontSize)));
|
||||
@@ -1,21 +0,0 @@
|
||||
// Listen for settings changes, the re-compute theme
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import type { ModelPayload, Settings } from "@yaakapp-internal/models";
|
||||
import { fireAndForget } from "./lib/fireAndForget";
|
||||
import { getSettings } from "./lib/settings";
|
||||
|
||||
function setFonts(settings: Settings) {
|
||||
document.documentElement.style.setProperty("--font-family-editor", settings.editorFont ?? "");
|
||||
document.documentElement.style.setProperty(
|
||||
"--font-family-interface",
|
||||
settings.interfaceFont ?? "",
|
||||
);
|
||||
}
|
||||
|
||||
listen<ModelPayload>("model_write", async (event) => {
|
||||
if (event.payload.change.type !== "upsert") return;
|
||||
if (event.payload.model.model !== "settings") return;
|
||||
setFonts(event.payload.model);
|
||||
}).catch(console.error);
|
||||
|
||||
fireAndForget(getSettings().then((settings) => setFonts(settings)));
|
||||
@@ -1,26 +0,0 @@
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { InlineCode } from "@yaakapp-internal/ui";
|
||||
import { showAlert } from "../lib/alert";
|
||||
import { appInfo } from "../lib/appInfo";
|
||||
import { minPromiseMillis } from "../lib/minPromiseMillis";
|
||||
import { invokeCmd } from "../lib/tauri";
|
||||
|
||||
export function useCheckForUpdates() {
|
||||
return useMutation({
|
||||
mutationKey: ["check_for_updates"],
|
||||
mutationFn: async () => {
|
||||
const hasUpdate: boolean = await minPromiseMillis(invokeCmd("cmd_check_for_updates"), 500);
|
||||
if (!hasUpdate) {
|
||||
showAlert({
|
||||
id: "no-updates",
|
||||
title: "No Update Available",
|
||||
body: (
|
||||
<>
|
||||
You are currently on the latest version <InlineCode>{appInfo.version}</InlineCode>
|
||||
</>
|
||||
),
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import type { HttpResponse } from "@yaakapp-internal/models";
|
||||
import { copyToClipboard } from "../lib/copy";
|
||||
import { getResponseBodyText } from "../lib/responseBody";
|
||||
import { useFastMutation } from "./useFastMutation";
|
||||
|
||||
export function useCopyHttpResponse(response: HttpResponse) {
|
||||
return useFastMutation({
|
||||
mutationKey: ["copy_http_response", response.id],
|
||||
async mutationFn() {
|
||||
const body = await getResponseBodyText({ response, filter: null });
|
||||
copyToClipboard(body);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import { createWorkspaceModel } from "@yaakapp-internal/models";
|
||||
import { jotaiStore } from "../lib/jotai";
|
||||
import { showPrompt } from "../lib/prompt";
|
||||
import { setWorkspaceSearchParams } from "../lib/setWorkspaceSearchParams";
|
||||
import { activeWorkspaceIdAtom } from "./useActiveWorkspace";
|
||||
import { useFastMutation } from "./useFastMutation";
|
||||
|
||||
export function useCreateCookieJar() {
|
||||
return useFastMutation({
|
||||
mutationKey: ["create_cookie_jar"],
|
||||
mutationFn: async () => {
|
||||
const workspaceId = jotaiStore.get(activeWorkspaceIdAtom);
|
||||
if (workspaceId == null) {
|
||||
throw new Error("Cannot create cookie jar when there's no active workspace");
|
||||
}
|
||||
|
||||
const name = await showPrompt({
|
||||
id: "new-cookie-jar",
|
||||
title: "New CookieJar",
|
||||
placeholder: "My Jar",
|
||||
confirmText: "Create",
|
||||
label: "Name",
|
||||
defaultValue: "My Jar",
|
||||
});
|
||||
if (name == null) return null;
|
||||
|
||||
return createWorkspaceModel({ model: "cookie_jar", workspaceId, name });
|
||||
},
|
||||
onSuccess: async (cookieJarId) => {
|
||||
setWorkspaceSearchParams({ cookie_jar_id: cookieJarId });
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { useCallback } from "react";
|
||||
import { CreateWorkspaceDialog } from "../components/CreateWorkspaceDialog";
|
||||
import { showDialog } from "../lib/dialog";
|
||||
|
||||
export function useCreateWorkspace() {
|
||||
return useCallback(() => {
|
||||
showDialog({
|
||||
id: "create-workspace",
|
||||
title: "Create Workspace",
|
||||
size: "sm",
|
||||
render: ({ hide }) => <CreateWorkspaceDialog hide={hide} />,
|
||||
});
|
||||
}, []);
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { invokeCmd } from "../lib/tauri";
|
||||
import { useFastMutation } from "./useFastMutation";
|
||||
|
||||
export function useDeleteGrpcConnections(requestId?: string) {
|
||||
return useFastMutation({
|
||||
mutationKey: ["delete_grpc_connections", requestId],
|
||||
mutationFn: async () => {
|
||||
if (requestId === undefined) return;
|
||||
await invokeCmd("cmd_delete_all_grpc_connections", { requestId });
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { invokeCmd } from "../lib/tauri";
|
||||
import { useFastMutation } from "./useFastMutation";
|
||||
|
||||
export function useDeleteHttpResponses(requestId?: string) {
|
||||
return useFastMutation({
|
||||
mutationKey: ["delete_http_responses", requestId],
|
||||
mutationFn: async () => {
|
||||
if (requestId === undefined) return;
|
||||
await invokeCmd("cmd_delete_all_http_responses", { requestId });
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
import {
|
||||
grpcConnectionsAtom,
|
||||
httpResponsesAtom,
|
||||
websocketConnectionsAtom,
|
||||
} from "@yaakapp-internal/models";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { showAlert } from "../lib/alert";
|
||||
import { showConfirmDelete } from "../lib/confirm";
|
||||
import { jotaiStore } from "../lib/jotai";
|
||||
import { pluralizeCount } from "../lib/pluralize";
|
||||
import { invokeCmd } from "../lib/tauri";
|
||||
import { activeWorkspaceIdAtom } from "./useActiveWorkspace";
|
||||
import { useFastMutation } from "./useFastMutation";
|
||||
|
||||
export function useDeleteSendHistory() {
|
||||
const httpResponses = useAtomValue(httpResponsesAtom);
|
||||
const grpcConnections = useAtomValue(grpcConnectionsAtom);
|
||||
const websocketConnections = useAtomValue(websocketConnectionsAtom);
|
||||
|
||||
const labels = [
|
||||
httpResponses.length > 0 ? pluralizeCount("Http Response", httpResponses.length) : null,
|
||||
grpcConnections.length > 0 ? pluralizeCount("Grpc Connection", grpcConnections.length) : null,
|
||||
websocketConnections.length > 0
|
||||
? pluralizeCount("WebSocket Connection", websocketConnections.length)
|
||||
: null,
|
||||
].filter((l) => l != null);
|
||||
|
||||
return useFastMutation({
|
||||
mutationKey: ["delete_send_history", labels],
|
||||
mutationFn: async () => {
|
||||
if (labels.length === 0) {
|
||||
showAlert({
|
||||
id: "no-responses",
|
||||
title: "Nothing to Delete",
|
||||
body: "There is no Http, Grpc, or Websocket history",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmed = await showConfirmDelete({
|
||||
id: "delete-send-history",
|
||||
title: "Clear Send History",
|
||||
description: <>Delete {labels.join(" and ")}?</>,
|
||||
});
|
||||
if (!confirmed) return false;
|
||||
|
||||
const workspaceId = jotaiStore.get(activeWorkspaceIdAtom);
|
||||
await invokeCmd("cmd_delete_send_history", { workspaceId });
|
||||
return true;
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import type { Environment } from "@yaakapp-internal/models";
|
||||
import { useKeyValue } from "./useKeyValue";
|
||||
|
||||
export function useEnvironmentValueVisibility(environment: Environment) {
|
||||
return useKeyValue<boolean>({
|
||||
namespace: "global",
|
||||
key: ["environmentValueVisibility", environment.workspaceId],
|
||||
fallback: false,
|
||||
});
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import { workspacesAtom } from "@yaakapp-internal/models";
|
||||
import { ExportDataDialog } from "../components/ExportDataDialog";
|
||||
import { showAlert } from "../lib/alert";
|
||||
import { showDialog } from "../lib/dialog";
|
||||
import { jotaiStore } from "../lib/jotai";
|
||||
import { showToast } from "../lib/toast";
|
||||
import { activeWorkspaceAtom } from "./useActiveWorkspace";
|
||||
import { useFastMutation } from "./useFastMutation";
|
||||
|
||||
export function useExportData() {
|
||||
return useFastMutation({
|
||||
mutationKey: ["export_data"],
|
||||
onError: (err: string) => {
|
||||
showAlert({ id: "export-failed", title: "Export Failed", body: err });
|
||||
},
|
||||
mutationFn: async () => {
|
||||
const activeWorkspace = jotaiStore.get(activeWorkspaceAtom);
|
||||
const workspaces = jotaiStore.get(workspacesAtom);
|
||||
|
||||
if (activeWorkspace == null || workspaces.length === 0) return;
|
||||
|
||||
showDialog({
|
||||
id: "export-data",
|
||||
title: "Export Data",
|
||||
size: "md",
|
||||
noPadding: true,
|
||||
render: ({ hide }) => (
|
||||
<ExportDataDialog
|
||||
onHide={hide}
|
||||
onSuccess={() => {
|
||||
showToast({
|
||||
color: "success",
|
||||
message: "Data export successful",
|
||||
});
|
||||
}}
|
||||
/>
|
||||
),
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { useAtomValue } from "jotai";
|
||||
import { activeWorkspaceAtom } from "./useActiveWorkspace";
|
||||
import { useKeyValue } from "./useKeyValue";
|
||||
|
||||
export function useFloatingSidebarHidden() {
|
||||
const activeWorkspace = useAtomValue(activeWorkspaceAtom);
|
||||
const { set, value } = useKeyValue<boolean>({
|
||||
namespace: "no_sync",
|
||||
key: ["floating_sidebar_hidden", activeWorkspace?.id ?? "n/a"],
|
||||
fallback: false,
|
||||
});
|
||||
|
||||
return [value, set] as const;
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { emit } from "@tauri-apps/api/event";
|
||||
import type { GrpcConnection, GrpcRequest } from "@yaakapp-internal/models";
|
||||
import { jotaiStore } from "../lib/jotai";
|
||||
import { minPromiseMillis } from "../lib/minPromiseMillis";
|
||||
import { invokeCmd } from "../lib/tauri";
|
||||
import { activeEnvironmentIdAtom, useActiveEnvironment } from "./useActiveEnvironment";
|
||||
import { useDebouncedValue } from "@yaakapp-internal/ui";
|
||||
|
||||
export interface ReflectResponseService {
|
||||
name: string;
|
||||
methods: { name: string; schema: string; serverStreaming: boolean; clientStreaming: boolean }[];
|
||||
}
|
||||
|
||||
export function useGrpc(
|
||||
req: GrpcRequest | null,
|
||||
conn: GrpcConnection | null,
|
||||
protoFiles: string[],
|
||||
) {
|
||||
const requestId = req?.id ?? "n/a";
|
||||
const environment = useActiveEnvironment();
|
||||
|
||||
const go = useMutation<void, string>({
|
||||
mutationKey: ["grpc_go", conn?.id],
|
||||
mutationFn: () =>
|
||||
invokeCmd<void>("cmd_grpc_go", { requestId, environmentId: environment?.id, protoFiles }),
|
||||
});
|
||||
|
||||
const send = useMutation({
|
||||
mutationKey: ["grpc_send", conn?.id],
|
||||
mutationFn: ({ message }: { message: string }) =>
|
||||
emit(`grpc_client_msg_${conn?.id ?? "none"}`, { Message: message }),
|
||||
});
|
||||
|
||||
const cancel = useMutation({
|
||||
mutationKey: ["grpc_cancel", conn?.id ?? "n/a"],
|
||||
mutationFn: () => emit(`grpc_client_msg_${conn?.id ?? "none"}`, "Cancel"),
|
||||
});
|
||||
|
||||
const commit = useMutation({
|
||||
mutationKey: ["grpc_commit", conn?.id ?? "n/a"],
|
||||
mutationFn: () => emit(`grpc_client_msg_${conn?.id ?? "none"}`, "Commit"),
|
||||
});
|
||||
|
||||
const debouncedUrl = useDebouncedValue<string>(req?.url ?? "", 1000);
|
||||
|
||||
const reflect = useQuery<ReflectResponseService[], string>({
|
||||
enabled: req != null,
|
||||
queryKey: ["grpc_reflect", req?.id ?? "n/a", debouncedUrl, protoFiles],
|
||||
staleTime: Infinity,
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
queryFn: () => {
|
||||
const environmentId = jotaiStore.get(activeEnvironmentIdAtom);
|
||||
return minPromiseMillis<ReflectResponseService[]>(
|
||||
invokeCmd("cmd_grpc_reflect", { requestId, protoFiles, environmentId }),
|
||||
300,
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
go,
|
||||
reflect,
|
||||
cancel,
|
||||
commit,
|
||||
isStreaming: conn != null && conn.state !== "closed",
|
||||
send,
|
||||
};
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import type { HttpResponse, HttpResponseEvent } from "@yaakapp-internal/models";
|
||||
import {
|
||||
httpResponseEventsAtom,
|
||||
mergeModelsInStore,
|
||||
replaceModelsInStore,
|
||||
} from "@yaakapp-internal/models";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { useEffect } from "react";
|
||||
import { fireAndForget } from "../lib/fireAndForget";
|
||||
|
||||
export function useHttpResponseEvents(response: HttpResponse | null) {
|
||||
const allEvents = useAtomValue(httpResponseEventsAtom);
|
||||
|
||||
useEffect(() => {
|
||||
if (response?.id == null) {
|
||||
replaceModelsInStore("http_response_event", []);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch events from database, filtering out events from other responses and merging atomically
|
||||
fireAndForget(
|
||||
invoke<HttpResponseEvent[]>("cmd_get_http_response_events", { responseId: response.id }).then(
|
||||
(events) =>
|
||||
mergeModelsInStore("http_response_event", events, (e) => e.responseId === response.id),
|
||||
),
|
||||
);
|
||||
}, [response?.id]);
|
||||
|
||||
const events = allEvents.filter((e) => e.responseId === response?.id);
|
||||
return { data: events, error: null, isLoading: false };
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import { installPluginFromDirectory } from "@yaakapp-internal/plugins";
|
||||
import { useFastMutation } from "./useFastMutation";
|
||||
|
||||
export function useInstallPlugin() {
|
||||
return useFastMutation<void, unknown, string>({
|
||||
mutationKey: ["install_plugin"],
|
||||
mutationFn: async (directory: string) => {
|
||||
await installPluginFromDirectory(directory);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
|
||||
import type { ModelPayload } from "@yaakapp-internal/models";
|
||||
import { atom, useAtomValue } from "jotai";
|
||||
import { generateId } from "../lib/generateId";
|
||||
import { jotaiStore } from "../lib/jotai";
|
||||
|
||||
const requestUpdateKeyAtom = atom<Record<string, string>>({});
|
||||
|
||||
getCurrentWebviewWindow()
|
||||
.listen<ModelPayload>("model_write", ({ payload }) => {
|
||||
if (payload.change.type !== "upsert") return;
|
||||
|
||||
if (
|
||||
(payload.model.model === "http_request" ||
|
||||
payload.model.model === "grpc_request" ||
|
||||
payload.model.model === "websocket_request") &&
|
||||
((payload.updateSource.type === "window" &&
|
||||
payload.updateSource.label !== getCurrentWebviewWindow().label) ||
|
||||
payload.updateSource.type !== "window")
|
||||
) {
|
||||
wasUpdatedExternally(payload.model.id);
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
|
||||
export function wasUpdatedExternally(changedRequestId: string) {
|
||||
jotaiStore.set(requestUpdateKeyAtom, (m) => ({ ...m, [changedRequestId]: generateId() }));
|
||||
}
|
||||
|
||||
export function useRequestUpdateKey(requestId: string | null) {
|
||||
const keys = useAtomValue(requestUpdateKeyAtom);
|
||||
const key = keys[requestId ?? "n/a"];
|
||||
return `${requestId}::${key ?? "default"}`;
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { settingsAtom } from "@yaakapp-internal/models";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { resolveAppearance } from "../lib/theme/appearance";
|
||||
import { usePreferredAppearance } from "./usePreferredAppearance";
|
||||
|
||||
export function useResolvedAppearance() {
|
||||
const preferredAppearance = usePreferredAppearance();
|
||||
const settings = useAtomValue(settingsAtom);
|
||||
return resolveAppearance(preferredAppearance, settings.appearance);
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import type { HttpResponse } from "@yaakapp-internal/models";
|
||||
import type { ServerSentEvent } from "@yaakapp-internal/sse";
|
||||
import { getResponseBodyEventSource } from "../lib/responseBody";
|
||||
|
||||
export function useResponseBodyEventSource(response: HttpResponse) {
|
||||
return useQuery<ServerSentEvent[]>({
|
||||
placeholderData: (prev) => prev, // Keep previous data on refetch
|
||||
queryKey: ["response-body-event-source", response.id, response.contentLength],
|
||||
queryFn: () => getResponseBodyEventSource(response),
|
||||
});
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import { useLocalStorage } from "react-use";
|
||||
|
||||
const DEFAULT_VIEW_MODE = "pretty";
|
||||
|
||||
export function useResponseViewMode(requestId?: string): [string, (m: "pretty" | "raw") => void] {
|
||||
const [value, setValue] = useLocalStorage<"pretty" | "raw">(`response_view_mode::${requestId}`);
|
||||
return [value ?? DEFAULT_VIEW_MODE, setValue];
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import { save } from "@tauri-apps/plugin-dialog";
|
||||
import type { HttpResponse } from "@yaakapp-internal/models";
|
||||
import { getModel } from "@yaakapp-internal/models";
|
||||
import mime from "mime";
|
||||
import slugify from "slugify";
|
||||
import { InlineCode } from "@yaakapp-internal/ui";
|
||||
import { getContentTypeFromHeaders } from "../lib/model_util";
|
||||
import { invokeCmd } from "../lib/tauri";
|
||||
import { showToast } from "../lib/toast";
|
||||
import { useFastMutation } from "./useFastMutation";
|
||||
|
||||
export function useSaveResponse(response: HttpResponse) {
|
||||
return useFastMutation({
|
||||
mutationKey: ["save_response", response.id],
|
||||
mutationFn: async () => {
|
||||
const request = getModel("http_request", response.requestId);
|
||||
if (request == null) return null;
|
||||
|
||||
const contentType = getContentTypeFromHeaders(response.headers) ?? "unknown";
|
||||
const ext = mime.getExtension(contentType);
|
||||
const slug = slugify(request.name || "response", { lower: true });
|
||||
const filepath = await save({
|
||||
defaultPath: ext ? `${slug}.${ext}` : slug,
|
||||
title: "Save Response",
|
||||
});
|
||||
await invokeCmd("cmd_save_response", { responseId: response.id, filepath });
|
||||
showToast({
|
||||
message: (
|
||||
<>
|
||||
Response saved to <InlineCode>{filepath}</InlineCode>
|
||||
</>
|
||||
),
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { useAtomValue } from "jotai";
|
||||
import { activeWorkspaceIdAtom } from "./useActiveWorkspace";
|
||||
import { useKeyValue } from "./useKeyValue";
|
||||
|
||||
export function useSidebarHidden() {
|
||||
const activeWorkspaceId = useAtomValue(activeWorkspaceIdAtom);
|
||||
const { set, value } = useKeyValue<boolean>({
|
||||
namespace: "no_sync",
|
||||
key: ["sidebar_hidden", activeWorkspaceId ?? "n/a"],
|
||||
fallback: false,
|
||||
});
|
||||
|
||||
return [value, set] as const;
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import { type } from "@tauri-apps/plugin-os";
|
||||
import { useIsFullscreen } from "@yaakapp-internal/ui";
|
||||
|
||||
export function useStoplightsVisible() {
|
||||
const fullscreen = useIsFullscreen();
|
||||
const stoplightsVisible = type() === "macos" && !fullscreen;
|
||||
return stoplightsVisible;
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { useHotKey } from "./useHotKey";
|
||||
import { useListenToTauriEvent } from "./useListenToTauriEvent";
|
||||
import { useZoom } from "./useZoom";
|
||||
|
||||
export function useSyncZoomSetting() {
|
||||
// Handle Zoom.
|
||||
// Note, Mac handles it in the app menu, so need to also handle keyboard
|
||||
// shortcuts for Windows/Linux
|
||||
const zoom = useZoom();
|
||||
useHotKey("app.zoom_in", zoom.zoomIn);
|
||||
useListenToTauriEvent("zoom_in", zoom.zoomIn);
|
||||
useHotKey("app.zoom_out", zoom.zoomOut);
|
||||
useListenToTauriEvent("zoom_out", zoom.zoomOut);
|
||||
useHotKey("app.zoom_reset", zoom.zoomReset);
|
||||
useListenToTauriEvent("zoom_reset", zoom.zoomReset);
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import type { Tokens } from "@yaakapp-internal/templates";
|
||||
import { invokeCmd } from "../lib/tauri";
|
||||
|
||||
export function useTemplateTokensToString(tokens: Tokens) {
|
||||
return useQuery<string>({
|
||||
refetchOnWindowFocus: false,
|
||||
queryKey: ["template_tokens_to_string", tokens],
|
||||
queryFn: () => templateTokensToString(tokens),
|
||||
});
|
||||
}
|
||||
|
||||
export async function templateTokensToString(tokens: Tokens): Promise<string> {
|
||||
return invokeCmd("cmd_template_tokens_to_string", { tokens });
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import type { TimelineViewMode } from "../components/HttpResponsePane";
|
||||
import { useKeyValue } from "./useKeyValue";
|
||||
|
||||
const DEFAULT_VIEW_MODE: TimelineViewMode = "timeline";
|
||||
|
||||
export function useTimelineViewMode() {
|
||||
const { set, value } = useKeyValue<TimelineViewMode>({
|
||||
namespace: "no_sync",
|
||||
key: "timeline_view_mode",
|
||||
fallback: DEFAULT_VIEW_MODE,
|
||||
});
|
||||
|
||||
return [value ?? DEFAULT_VIEW_MODE, set] as const;
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { useCallback } from "react";
|
||||
import { CommandPaletteDialog } from "../components/CommandPaletteDialog";
|
||||
import { toggleDialog } from "../lib/dialog";
|
||||
|
||||
export function useToggleCommandPalette() {
|
||||
const togglePalette = useCallback(() => {
|
||||
toggleDialog({
|
||||
id: "command_palette",
|
||||
size: "dynamic",
|
||||
hideX: true,
|
||||
className: "mb-auto mt-[4rem] !max-h-[min(30rem,calc(100vh-4rem))]",
|
||||
vAlign: "top",
|
||||
noPadding: true,
|
||||
noScroll: true,
|
||||
render: ({ hide }) => <CommandPaletteDialog onClose={hide} />,
|
||||
});
|
||||
}, []);
|
||||
|
||||
return togglePalette;
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Yaak App</title>
|
||||
<!-- <script src="http://localhost:8097"></script>-->
|
||||
|
||||
<!-- Certain elements like webview (and maybe <select>?) will use background
|
||||
color depending on document background color-->
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html,
|
||||
body {
|
||||
background-color: #1b1a29;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="text-base">
|
||||
<div id="root"></div>
|
||||
<div id="cm-portal" class="cm-portal"></div>
|
||||
<div id="react-portal"></div>
|
||||
<div id="radix-portal" class="cm-portal"></div>
|
||||
<script type="module" src="/theme.ts"></script>
|
||||
<script type="module" src="/font-size.ts"></script>
|
||||
<script type="module" src="/font.ts"></script>
|
||||
<script type="module" src="/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,30 +0,0 @@
|
||||
import type { AlertProps } from "../components/core/Alert";
|
||||
import { Alert } from "../components/core/Alert";
|
||||
import type { DialogProps } from "../components/core/Dialog";
|
||||
import { showDialog } from "./dialog";
|
||||
|
||||
interface AlertArgs {
|
||||
id: string;
|
||||
title: DialogProps["title"];
|
||||
body: AlertProps["body"];
|
||||
size?: DialogProps["size"];
|
||||
}
|
||||
|
||||
export function showAlert({ id, title, body, size = "sm" }: AlertArgs) {
|
||||
showDialog({
|
||||
id,
|
||||
title,
|
||||
hideX: true,
|
||||
size,
|
||||
disableBackdropClose: true, // Prevent accidental dismisses
|
||||
render: ({ hide }) => Alert({ onHide: hide, body }),
|
||||
});
|
||||
}
|
||||
|
||||
export function showSimpleAlert(title: string, message: string) {
|
||||
showAlert({
|
||||
id: "simple-alert",
|
||||
body: message,
|
||||
title: title,
|
||||
});
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import deepEqual from "@gilbarbara/deep-equal";
|
||||
import type { UpdateInfo } from "@yaakapp-internal/tauri-client";
|
||||
import type { Atom } from "jotai";
|
||||
import { atom } from "jotai";
|
||||
import { selectAtom } from "jotai/utils";
|
||||
import type { SplitLayoutLayout } from "@yaakapp-internal/ui";
|
||||
import { atomWithKVStorage } from "./atoms/atomWithKVStorage";
|
||||
|
||||
export function deepEqualAtom<T>(a: Atom<T>) {
|
||||
return selectAtom(
|
||||
a,
|
||||
(v) => v,
|
||||
(a, b) => deepEqual(a, b),
|
||||
);
|
||||
}
|
||||
|
||||
export const workspaceLayoutAtom = atomWithKVStorage<SplitLayoutLayout>(
|
||||
"workspace_layout",
|
||||
"horizontal",
|
||||
);
|
||||
|
||||
export const updateAvailableAtom = atom<Omit<UpdateInfo, "replyEventId"> | null>(null);
|
||||
@@ -1,99 +0,0 @@
|
||||
import MimeType from "whatwg-mimetype";
|
||||
import type { EditorProps } from "../components/core/Editor/Editor";
|
||||
|
||||
export function languageFromContentType(
|
||||
contentType: string | null,
|
||||
content: string | null = null,
|
||||
): EditorProps["language"] {
|
||||
const justContentType = contentType?.split(";")[0] ?? contentType ?? "";
|
||||
if (justContentType.includes("json")) {
|
||||
return "json";
|
||||
}
|
||||
if (justContentType.includes("xml")) {
|
||||
return "xml";
|
||||
}
|
||||
if (justContentType.includes("html")) {
|
||||
const detected = languageFromContent(content);
|
||||
if (detected === "xml") {
|
||||
// If it's detected as XML, but is already HTML, don't change it
|
||||
return "html";
|
||||
}
|
||||
return detected;
|
||||
}
|
||||
if (justContentType.includes("javascript")) {
|
||||
// Sometimes `application/javascript` returns JSON, so try detecting that
|
||||
return languageFromContent(content, "javascript");
|
||||
}
|
||||
if (justContentType.includes("markdown")) {
|
||||
return "markdown";
|
||||
}
|
||||
|
||||
return languageFromContent(content, "text");
|
||||
}
|
||||
|
||||
export function languageFromContent(
|
||||
content: string | null,
|
||||
fallback?: EditorProps["language"],
|
||||
): EditorProps["language"] {
|
||||
if (content == null) return "text";
|
||||
|
||||
const firstBytes = content.slice(0, 20).trim();
|
||||
|
||||
if (firstBytes.startsWith("{") || firstBytes.startsWith("[")) {
|
||||
return "json";
|
||||
}
|
||||
if (
|
||||
firstBytes.toLowerCase().startsWith("<!doctype") ||
|
||||
firstBytes.toLowerCase().startsWith("<html")
|
||||
) {
|
||||
return "html";
|
||||
}
|
||||
if (firstBytes.startsWith("<")) {
|
||||
return "xml";
|
||||
}
|
||||
|
||||
return fallback;
|
||||
}
|
||||
|
||||
export function isJSON(content: string | null | undefined): boolean {
|
||||
if (typeof content !== "string") return false;
|
||||
|
||||
try {
|
||||
JSON.parse(content);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function isProbablyTextContentType(contentType: string | null): boolean {
|
||||
if (contentType == null) return false;
|
||||
|
||||
const mimeType = getMimeTypeFromContentType(contentType).essence;
|
||||
const normalized = mimeType.toLowerCase();
|
||||
|
||||
// Check if it starts with "text/"
|
||||
if (normalized.startsWith("text/")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Common text mimetypes and suffixes
|
||||
return [
|
||||
"application/json",
|
||||
"application/xml",
|
||||
"application/javascript",
|
||||
"application/yaml",
|
||||
"+json",
|
||||
"+xml",
|
||||
"+yaml",
|
||||
"+text",
|
||||
].some((textType) => normalized === textType || normalized.endsWith(textType));
|
||||
}
|
||||
|
||||
export function getMimeTypeFromContentType(contentType: string): MimeType {
|
||||
try {
|
||||
return new MimeType(contentType);
|
||||
} catch {
|
||||
return new MimeType("text/plain");
|
||||
}
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
export const charsets = [
|
||||
"utf-8",
|
||||
"us-ascii",
|
||||
"950",
|
||||
"ASMO-708",
|
||||
"CP1026",
|
||||
"CP870",
|
||||
"DOS-720",
|
||||
"DOS-862",
|
||||
"EUC-CN",
|
||||
"IBM437",
|
||||
"Johab",
|
||||
"Windows-1252",
|
||||
"X-EBCDIC-Spain",
|
||||
"big5",
|
||||
"cp866",
|
||||
"csISO2022JP",
|
||||
"ebcdic-cp-us",
|
||||
"euc-kr",
|
||||
"gb2312",
|
||||
"hz-gb-2312",
|
||||
"ibm737",
|
||||
"ibm775",
|
||||
"ibm850",
|
||||
"ibm852",
|
||||
"ibm857",
|
||||
"ibm861",
|
||||
"ibm869",
|
||||
"iso-2022-jp",
|
||||
"iso-2022-jp",
|
||||
"iso-2022-kr",
|
||||
"iso-8859-1",
|
||||
"iso-8859-15",
|
||||
"iso-8859-2",
|
||||
"iso-8859-3",
|
||||
"iso-8859-4",
|
||||
"iso-8859-5",
|
||||
"iso-8859-6",
|
||||
"iso-8859-7",
|
||||
"iso-8859-8",
|
||||
"iso-8859-8-i",
|
||||
"iso-8859-9",
|
||||
"koi8-r",
|
||||
"koi8-u",
|
||||
"ks_c_5601-1987",
|
||||
"macintosh",
|
||||
"shift_jis",
|
||||
"unicode",
|
||||
"unicodeFFFE",
|
||||
"utf-7",
|
||||
"windows-1250",
|
||||
"windows-1251",
|
||||
"windows-1253",
|
||||
"windows-1254",
|
||||
"windows-1255",
|
||||
"windows-1256",
|
||||
"windows-1257",
|
||||
"windows-1258",
|
||||
"windows-874",
|
||||
"x-Chinese-CNS",
|
||||
"x-Chinese-Eten",
|
||||
"x-EBCDIC-Arabic",
|
||||
"x-EBCDIC-CyrillicRussian",
|
||||
"x-EBCDIC-CyrillicSerbianBulgarian",
|
||||
"x-EBCDIC-DenmarkNorway",
|
||||
"x-EBCDIC-FinlandSweden",
|
||||
"x-EBCDIC-Germany",
|
||||
"x-EBCDIC-Greek",
|
||||
"x-EBCDIC-GreekModern",
|
||||
"x-EBCDIC-Hebrew",
|
||||
"x-EBCDIC-Icelandic",
|
||||
"x-EBCDIC-Italy",
|
||||
"x-EBCDIC-JapaneseAndJapaneseLatin",
|
||||
"x-EBCDIC-JapaneseAndKana",
|
||||
"x-EBCDIC-JapaneseAndUSCanada",
|
||||
"x-EBCDIC-JapaneseKatakana",
|
||||
"x-EBCDIC-KoreanAndKoreanExtended",
|
||||
"x-EBCDIC-KoreanExtended",
|
||||
"x-EBCDIC-SimplifiedChinese",
|
||||
"x-EBCDIC-Thai",
|
||||
"x-EBCDIC-TraditionalChinese",
|
||||
"x-EBCDIC-Turkish",
|
||||
"x-EBCDIC-UK",
|
||||
"x-Europa",
|
||||
"x-IA5",
|
||||
"x-IA5-German",
|
||||
"x-IA5-Norwegian",
|
||||
"x-IA5-Swedish",
|
||||
"x-ebcdic-cp-us-euro",
|
||||
"x-ebcdic-denmarknorway-euro",
|
||||
"x-ebcdic-finlandsweden-euro",
|
||||
"x-ebcdic-finlandsweden-euro",
|
||||
"x-ebcdic-france-euro",
|
||||
"x-ebcdic-germany-euro",
|
||||
"x-ebcdic-icelandic-euro",
|
||||
"x-ebcdic-international-euro",
|
||||
"x-ebcdic-italy-euro",
|
||||
"x-ebcdic-spain-euro",
|
||||
"x-ebcdic-uk-euro",
|
||||
"x-euc-jp",
|
||||
"x-iscii-as",
|
||||
"x-iscii-be",
|
||||
"x-iscii-de",
|
||||
"x-iscii-gu",
|
||||
"x-iscii-ka",
|
||||
"x-iscii-ma",
|
||||
"x-iscii-or",
|
||||
"x-iscii-pa",
|
||||
"x-iscii-ta",
|
||||
"x-iscii-te",
|
||||
"x-mac-arabic",
|
||||
"x-mac-ce",
|
||||
"x-mac-chinesesimp",
|
||||
"x-mac-cyrillic",
|
||||
"x-mac-greek",
|
||||
"x-mac-hebrew",
|
||||
"x-mac-icelandic",
|
||||
"x-mac-japanese",
|
||||
"x-mac-korean",
|
||||
"x-mac-turkish",
|
||||
];
|
||||
@@ -1 +0,0 @@
|
||||
export const connections = ["close", "keep-alive"];
|
||||
@@ -1 +0,0 @@
|
||||
export const encodings = ["*", "gzip", "compress", "deflate", "br", "zstd", "identity"];
|
||||
@@ -1,70 +0,0 @@
|
||||
import type { GenericCompletionOption } from "@yaakapp-internal/plugins";
|
||||
|
||||
export const headerNames: (GenericCompletionOption | string)[] = [
|
||||
{
|
||||
type: "constant",
|
||||
label: "Content-Type",
|
||||
info: "The original media type of the resource (prior to any content encoding applied for sending)",
|
||||
},
|
||||
{
|
||||
type: "constant",
|
||||
label: "Content-Length",
|
||||
info: "The size of the message body, in bytes, sent to the recipient",
|
||||
},
|
||||
{
|
||||
type: "constant",
|
||||
label: "Accept",
|
||||
info:
|
||||
"The content types, expressed as MIME types, the client is able to understand. " +
|
||||
"The server uses content negotiation to select one of the proposals and informs " +
|
||||
"the client of the choice with the Content-Type response header. Browsers set required " +
|
||||
"values for this header based on the context of the request. For example, a browser uses " +
|
||||
"different values in a request when fetching a CSS stylesheet, image, video, or a script.",
|
||||
},
|
||||
{
|
||||
type: "constant",
|
||||
label: "Accept-Encoding",
|
||||
info:
|
||||
"The content encoding (usually a compression algorithm) that the client can understand. " +
|
||||
"The server uses content negotiation to select one of the proposals and informs the client " +
|
||||
"of that choice with the Content-Encoding response header.",
|
||||
},
|
||||
{
|
||||
type: "constant",
|
||||
label: "Accept-Language",
|
||||
info:
|
||||
"The natural language and locale that the client prefers. The server uses content " +
|
||||
"negotiation to select one of the proposals and informs the client of the choice with " +
|
||||
"the Content-Language response header.",
|
||||
},
|
||||
{
|
||||
type: "constant",
|
||||
label: "Authorization",
|
||||
info: "Provide credentials that authenticate a user agent with a server, allowing access to a protected resource.",
|
||||
},
|
||||
"Cache-Control",
|
||||
"Cookie",
|
||||
"Connection",
|
||||
"Content-MD5",
|
||||
"Date",
|
||||
"Expect",
|
||||
"Forwarded",
|
||||
"From",
|
||||
"Host",
|
||||
"If-Match",
|
||||
"If-Modified-Since",
|
||||
"If-None-Match",
|
||||
"If-Range",
|
||||
"If-Unmodified-Since",
|
||||
"Max-Forwards",
|
||||
"Origin",
|
||||
"Pragma",
|
||||
"Proxy-Authorization",
|
||||
"Range",
|
||||
"Referer",
|
||||
"TE",
|
||||
"User-Agent",
|
||||
"Upgrade",
|
||||
"Via",
|
||||
"Warning",
|
||||
];
|
||||
@@ -1,213 +0,0 @@
|
||||
export const mimeTypes = [
|
||||
"application/json",
|
||||
"application/xml",
|
||||
"application/x-www-form-urlencoded",
|
||||
"multipart/form-data",
|
||||
"multipart/byteranges",
|
||||
"application/octet-stream",
|
||||
"text/plain",
|
||||
"application/javascript",
|
||||
"application/pdf",
|
||||
"text/html",
|
||||
"image/png",
|
||||
"image/jpeg",
|
||||
"image/gif",
|
||||
"image/webp",
|
||||
"text/css",
|
||||
"application/x-pkcs12",
|
||||
"application/xhtml+xml",
|
||||
"application/andrew-inset",
|
||||
"application/applixware",
|
||||
"application/atom+xml",
|
||||
"application/atomcat+xml",
|
||||
"application/atomsvc+xml",
|
||||
"application/bdoc",
|
||||
"application/cu-seeme",
|
||||
"application/davmount+xml",
|
||||
"application/docbook+xml",
|
||||
"application/dssc+xml",
|
||||
"application/ecmascript",
|
||||
"application/epub+zip",
|
||||
"application/exi",
|
||||
"application/font-tdpfr",
|
||||
"application/font-woff",
|
||||
"application/font-woff2",
|
||||
"application/geo+json",
|
||||
"application/graphql",
|
||||
"application/java-serialized-object",
|
||||
"application/json5",
|
||||
"application/jsonml+json",
|
||||
"application/ld+json",
|
||||
"application/lost+xml",
|
||||
"application/manifest+json",
|
||||
"application/mp4",
|
||||
"application/msword",
|
||||
"application/mxf",
|
||||
"application/n-triples",
|
||||
"application/n-quads",
|
||||
"application/oda",
|
||||
"application/ogg",
|
||||
"application/pgp-encrypted",
|
||||
"application/pgp-signature",
|
||||
"application/pics-rules",
|
||||
"application/pkcs10",
|
||||
"application/pkcs7-mime",
|
||||
"application/pkcs7-signature",
|
||||
"application/pkcs8",
|
||||
"application/postscript",
|
||||
"application/pskc+xml",
|
||||
"application/rdf+xml",
|
||||
"application/resource-lists+xml",
|
||||
"application/resource-lists-diff+xml",
|
||||
"application/rls-services+xml",
|
||||
"application/rsd+xml",
|
||||
"application/rss+xml",
|
||||
"application/rtf",
|
||||
"application/sdp",
|
||||
"application/shf+xml",
|
||||
"application/timestamped-data",
|
||||
"application/trig",
|
||||
"application/vnd.android.package-archive",
|
||||
"application/vnd.api+json",
|
||||
"application/vnd.apple.installer+xml",
|
||||
"application/vnd.apple.mpegurl",
|
||||
"application/vnd.apple.pkpass",
|
||||
"application/vnd.bmi",
|
||||
"application/vnd.curl.car",
|
||||
"application/vnd.curl.pcurl",
|
||||
"application/vnd.dna",
|
||||
"application/vnd.google-apps.document",
|
||||
"application/vnd.google-apps.presentation",
|
||||
"application/vnd.google-apps.spreadsheet",
|
||||
"application/vnd.hal+xml",
|
||||
"application/vnd.handheld-entertainment+xml",
|
||||
"application/vnd.macports.portpkg",
|
||||
"application/vnd.unity",
|
||||
"application/vnd.zul",
|
||||
"application/widget",
|
||||
"application/wsdl+xml",
|
||||
"application/x-7z-compressed",
|
||||
"application/x-ace-compressed",
|
||||
"application/x-bittorrent",
|
||||
"application/x-bzip",
|
||||
"application/x-bzip2",
|
||||
"application/x-cfs-compressed",
|
||||
"application/x-chrome-extension",
|
||||
"application/x-cocoa",
|
||||
"application/x-envoy",
|
||||
"application/x-eva",
|
||||
"font/opentype",
|
||||
"application/x-gca-compressed",
|
||||
"application/x-gtar",
|
||||
"application/x-hdf",
|
||||
"application/x-httpd-php",
|
||||
"application/x-install-instructions",
|
||||
"application/x-latex",
|
||||
"application/x-lua-bytecode",
|
||||
"application/x-lzh-compressed",
|
||||
"application/x-ms-application",
|
||||
"application/x-ms-shortcut",
|
||||
"application/x-ndjson",
|
||||
"application/x-perl",
|
||||
"application/x-pkcs7-certificates",
|
||||
"application/x-pkcs7-certreqresp",
|
||||
"application/x-rar-compressed",
|
||||
"application/x-sh",
|
||||
"application/x-sql",
|
||||
"application/x-subrip",
|
||||
"application/x-t3vm-image",
|
||||
"application/x-tads",
|
||||
"application/x-tar",
|
||||
"application/x-tcl",
|
||||
"application/x-tex",
|
||||
"application/x-x509-ca-cert",
|
||||
"application/xop+xml",
|
||||
"application/xslt+xml",
|
||||
"application/zip",
|
||||
"audio/3gpp",
|
||||
"audio/adpcm",
|
||||
"audio/basic",
|
||||
"audio/midi",
|
||||
"audio/mpeg",
|
||||
"audio/mp4",
|
||||
"audio/ogg",
|
||||
"audio/silk",
|
||||
"audio/wave",
|
||||
"audio/webm",
|
||||
"audio/x-aac",
|
||||
"audio/x-aiff",
|
||||
"audio/x-caf",
|
||||
"audio/x-flac",
|
||||
"audio/xm",
|
||||
"image/bmp",
|
||||
"image/cgm",
|
||||
"image/sgi",
|
||||
"image/svg+xml",
|
||||
"image/tiff",
|
||||
"image/x-3ds",
|
||||
"image/x-freehand",
|
||||
"image/x-icon",
|
||||
"image/x-jng",
|
||||
"image/x-mrsid-image",
|
||||
"image/x-pcx",
|
||||
"image/x-pict",
|
||||
"image/x-rgb",
|
||||
"image/x-tga",
|
||||
"message/rfc822",
|
||||
"text/cache-manifest",
|
||||
"text/calendar",
|
||||
"text/coffeescript",
|
||||
"text/csv",
|
||||
"text/hjson",
|
||||
"text/jade",
|
||||
"text/jsx",
|
||||
"text/less",
|
||||
"text/mathml",
|
||||
"text/n3",
|
||||
"text/richtext",
|
||||
"text/sgml",
|
||||
"text/slim",
|
||||
"text/stylus",
|
||||
"text/tab-separated-values",
|
||||
"text/turtle",
|
||||
"text/uri-list",
|
||||
"text/vcard",
|
||||
"text/vnd.curl",
|
||||
"text/vnd.fly",
|
||||
"text/vtt",
|
||||
"text/x-asm",
|
||||
"text/x-c",
|
||||
"text/x-component",
|
||||
"text/x-fortran",
|
||||
"text/x-handlebars-template",
|
||||
"text/x-java-source",
|
||||
"text/x-lua",
|
||||
"text/x-markdown",
|
||||
"text/x-nfo",
|
||||
"text/x-opml",
|
||||
"text/x-pascal",
|
||||
"text/x-processing",
|
||||
"text/x-sass",
|
||||
"text/x-scss",
|
||||
"text/x-vcalendar",
|
||||
"text/xml",
|
||||
"text/yaml",
|
||||
"video/3gpp",
|
||||
"video/3gpp2",
|
||||
"video/h261",
|
||||
"video/h263",
|
||||
"video/h264",
|
||||
"video/jpeg",
|
||||
"video/jpm",
|
||||
"video/mj2",
|
||||
"video/mp2t",
|
||||
"video/mp4",
|
||||
"video/mpeg",
|
||||
"video/ogg",
|
||||
"video/quicktime",
|
||||
"video/webm",
|
||||
"video/x-f4v",
|
||||
"video/x-fli",
|
||||
"video/x-flv",
|
||||
"video/x-m4v",
|
||||
];
|
||||
@@ -1,19 +0,0 @@
|
||||
import type { Folder, GrpcRequest, HttpRequest, WebsocketRequest } from "@yaakapp-internal/models";
|
||||
import { duplicateModel } from "@yaakapp-internal/models";
|
||||
import { activeWorkspaceIdAtom } from "../hooks/useActiveWorkspace";
|
||||
import { jotaiStore } from "./jotai";
|
||||
import { navigateToRequestOrFolderOrWorkspace } from "./setWorkspaceSearchParams";
|
||||
|
||||
export async function duplicateRequestOrFolderAndNavigate(
|
||||
model: Folder | HttpRequest | GrpcRequest | WebsocketRequest | null,
|
||||
) {
|
||||
if (model == null) {
|
||||
throw new Error("Cannot duplicate null item");
|
||||
}
|
||||
|
||||
const newId = await duplicateModel(model);
|
||||
const workspaceId = jotaiStore.get(activeWorkspaceIdAtom);
|
||||
if (workspaceId == null || model.model === "folder") return;
|
||||
|
||||
navigateToRequestOrFolderOrWorkspace(newId, model.model);
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
import { parseTemplate } from "@yaakapp-internal/templates";
|
||||
import { activeEnvironmentIdAtom } from "../hooks/useActiveEnvironment";
|
||||
import { activeWorkspaceIdAtom } from "../hooks/useActiveWorkspace";
|
||||
import { jotaiStore } from "./jotai";
|
||||
import { invokeCmd } from "./tauri";
|
||||
|
||||
export function analyzeTemplate(template: string): "global_secured" | "local_secured" | "insecure" {
|
||||
let secureTags = 0;
|
||||
let insecureTags = 0;
|
||||
let totalTags = 0;
|
||||
for (const t of parseTemplate(template).tokens) {
|
||||
if (t.type === "eof") continue;
|
||||
|
||||
totalTags++;
|
||||
if (t.type === "tag" && t.val.type === "fn" && t.val.name === "secure") {
|
||||
secureTags++;
|
||||
} else if (t.type === "tag" && t.val.type === "var") {
|
||||
// Variables are secure
|
||||
} else if (t.type === "tag" && t.val.type === "bool") {
|
||||
// Booleans are secure
|
||||
} else {
|
||||
insecureTags++;
|
||||
}
|
||||
}
|
||||
|
||||
if (secureTags === 1 && totalTags === 1) {
|
||||
return "global_secured";
|
||||
}
|
||||
if (insecureTags === 0) {
|
||||
return "local_secured";
|
||||
}
|
||||
return "insecure";
|
||||
}
|
||||
|
||||
export async function convertTemplateToInsecure(template: string) {
|
||||
if (template === "") {
|
||||
return "";
|
||||
}
|
||||
|
||||
const workspaceId = jotaiStore.get(activeWorkspaceIdAtom) ?? "n/a";
|
||||
const environmentId = jotaiStore.get(activeEnvironmentIdAtom) ?? null;
|
||||
return invokeCmd<string>("cmd_decrypt_template", { template, workspaceId, environmentId });
|
||||
}
|
||||
|
||||
export async function convertTemplateToSecure(template: string): Promise<string> {
|
||||
if (template === "") {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (analyzeTemplate(template) === "global_secured") {
|
||||
return template;
|
||||
}
|
||||
|
||||
const workspaceId = jotaiStore.get(activeWorkspaceIdAtom) ?? "n/a";
|
||||
const environmentId = jotaiStore.get(activeEnvironmentIdAtom) ?? null;
|
||||
return invokeCmd<string>("cmd_secure_template", { template, workspaceId, environmentId });
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { showErrorToast } from "./toast";
|
||||
|
||||
/**
|
||||
* Handles a fire-and-forget promise by catching and reporting errors
|
||||
* via console.error and a toast notification.
|
||||
*/
|
||||
export function fireAndForget(promise: Promise<unknown>) {
|
||||
promise.catch((err: unknown) => {
|
||||
console.error("Unhandled async error:", err);
|
||||
showErrorToast({
|
||||
id: "async-error",
|
||||
title: "Unexpected Error",
|
||||
message: err instanceof Error ? err.message : String(err),
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
import vkBeautify from "vkbeautify";
|
||||
import { invokeCmd } from "./tauri";
|
||||
|
||||
export async function tryFormatJson(text: string): Promise<string> {
|
||||
if (text === "") return text;
|
||||
|
||||
try {
|
||||
const result = await invokeCmd<string>("cmd_format_json", { text });
|
||||
return result;
|
||||
} catch (err) {
|
||||
console.warn("Failed to format JSON", err);
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(text), null, 2);
|
||||
} catch (err) {
|
||||
console.log("JSON beautify failed", err);
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
export async function tryFormatGraphql(text: string): Promise<string> {
|
||||
if (text === "") return text;
|
||||
|
||||
try {
|
||||
return await invokeCmd<string>("cmd_format_graphql", { text });
|
||||
} catch (err) {
|
||||
console.warn("Failed to format GraphQL", err);
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
export async function tryFormatXml(text: string): Promise<string> {
|
||||
if (text === "") return text;
|
||||
|
||||
try {
|
||||
return vkBeautify.xml(text, " ");
|
||||
} catch (err) {
|
||||
console.warn("Failed to format XML", err);
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user