mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-09-02 08:37:18 +02:00
Compare commits
76
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
50cccf1d25 | ||
|
|
0aae516f3a | ||
|
|
b7b8ae5f94 | ||
|
|
6777003b70 | ||
|
|
426c6d5eb6 | ||
|
|
068afe325e | ||
|
|
cef1129d4b | ||
|
|
9a7bcf73bb | ||
|
|
aa76d501f0 | ||
|
|
ce8cb5e7bc | ||
|
|
66c13d7e13 | ||
|
|
388dd8815f | ||
|
|
cb295b7102 | ||
|
|
4e835cf7e8 | ||
|
|
2350689d7a | ||
|
|
4eebd606ef | ||
|
|
c4f96f3f11 | ||
|
|
71c217d3f0 | ||
|
|
d89831a84d | ||
|
|
3332ae263f | ||
|
|
df1fd864b2 | ||
|
|
a2d54ca774 | ||
|
|
36fec8b005 | ||
|
|
115615d994 | ||
|
|
538f782068 | ||
|
|
3f202ff664 | ||
|
|
131b7e5ab1 | ||
|
|
69083918f9 | ||
|
|
4b2dcf9a1a | ||
|
|
569f552d79 | ||
|
|
33f32cccf6 | ||
|
|
7ca772347f | ||
|
|
b89c448345 | ||
|
|
2021df112a | ||
|
|
2d2a390bfd | ||
|
|
5cce23566a | ||
|
|
e99f6d2bc7 | ||
|
|
bea58b16b4 | ||
|
|
93fba4d9b4 | ||
|
|
b9071eafe0 | ||
|
|
778c74c635 | ||
|
|
0f434361a7 | ||
|
|
d27d11af7c | ||
|
|
1a19a06a23 | ||
|
|
07a9a6c6c0 | ||
|
|
e54240d579 | ||
|
|
8bca013ab4 | ||
|
|
10e962a0e6 | ||
|
|
78954e10c8 | ||
|
|
9eb7a001da | ||
|
|
6a02cbe525 | ||
|
|
6f91f76064 | ||
|
|
32e92d484b | ||
|
|
cdbbef34f8 | ||
|
|
4838353585 | ||
|
|
93001e3da7 | ||
|
|
b31c066717 | ||
|
|
5d1d24870a | ||
|
|
6f0d0ef275 | ||
|
|
85a9b2a908 | ||
|
|
f3f05502d1 | ||
|
|
2e0f7d1818 | ||
|
|
dc793181bb | ||
|
|
7dfa7e07e3 | ||
|
|
23e7229e63 | ||
|
|
1f91cddab9 | ||
|
|
be004425fa | ||
|
|
4f03c5c390 | ||
|
|
cb43f0b9b9 | ||
|
|
68671377fd | ||
|
|
2383f06e71 | ||
|
|
0068be9ffc | ||
|
|
def7d752db | ||
|
|
85aed740a8 | ||
|
|
d3055c64b3 | ||
|
|
912aaeb7a7 |
@@ -0,0 +1,11 @@
|
|||||||
|
node_modules
|
||||||
|
**/node_modules
|
||||||
|
dist
|
||||||
|
**/dist
|
||||||
|
target
|
||||||
|
**/target
|
||||||
|
.claude
|
||||||
|
vendored
|
||||||
|
**/vendored
|
||||||
|
*.log
|
||||||
|
.git
|
||||||
@@ -31,3 +31,7 @@ jobs:
|
|||||||
run: vp test
|
run: vp test
|
||||||
- name: Run Rust Tests
|
- name: Run Rust Tests
|
||||||
run: cargo test --all --features yaak-app-client/wry
|
run: cargo test --all --features yaak-app-client/wry
|
||||||
|
- name: OpenAPI import round-trip
|
||||||
|
run: |
|
||||||
|
cargo build -p yaak-cli
|
||||||
|
node plugins/importer-openapi/tests/roundtrip.mjs
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ jobs:
|
|||||||
runtime: "wry"
|
runtime: "wry"
|
||||||
targets: "aarch64-pc-windows-msvc"
|
targets: "aarch64-pc-windows-msvc"
|
||||||
runs-on: ${{ matrix.platform }}
|
runs-on: ${{ matrix.platform }}
|
||||||
timeout-minutes: 40
|
timeout-minutes: 60
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout yaakapp/app
|
- name: Checkout yaakapp/app
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -103,6 +103,18 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y cmake ninja-build libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev libnss3 patchelf xdg-utils
|
sudo apt-get install -y cmake ninja-build libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev libnss3 patchelf xdg-utils
|
||||||
|
# crates/yaak-wasm compiles SQLite to wasm via sqlite-wasm-rs, whose C shim
|
||||||
|
# uses C23 [[noreturn]] and expects a freestanding wasm32 target. Ubuntu
|
||||||
|
# 22.04 ships only clang <=15: 14 rejects the attribute, and 15 falls
|
||||||
|
# through to host glibc headers ("bits/libc-header-start.h" not found).
|
||||||
|
# clang-18 handles it (it is what ubuntu-24.04 uses). Install it from
|
||||||
|
# apt.llvm.org since 22.04's repos stop at 15. Only the wasm build uses
|
||||||
|
# this compiler, so the shipped binary keeps 22.04's glibc floor.
|
||||||
|
wget -qO /tmp/llvm.sh https://apt.llvm.org/llvm.sh
|
||||||
|
chmod +x /tmp/llvm.sh
|
||||||
|
sudo /tmp/llvm.sh 18
|
||||||
|
echo "CC_wasm32_unknown_unknown=/usr/bin/clang-18" >> "$GITHUB_ENV"
|
||||||
|
echo "AR_wasm32_unknown_unknown=/usr/bin/llvm-ar-18" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
- name: Install Protoc for plugin-runtime
|
- name: Install Protoc for plugin-runtime
|
||||||
uses: arduino/setup-protoc@v3
|
uses: arduino/setup-protoc@v3
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
name: Release web image
|
||||||
|
|
||||||
|
# Builds ghcr.io/mountain-loop/yaak-web: the browser client and the server that serves it.
|
||||||
|
# One image per architecture on its own native runner (emulating a Rust release build is hours),
|
||||||
|
# joined into one multi-arch tag at the end.
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags: [v*]
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
version:
|
||||||
|
description: Version to publish, without the v (e.g. 2026.2.0). Empty publishes main and sha tags only.
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
env:
|
||||||
|
IMAGE: ghcr.io/mountain-loop/yaak-web
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
if: github.repository == 'mountain-loop/yaak'
|
||||||
|
name: Build ${{ matrix.platform }}
|
||||||
|
runs-on: ${{ matrix.runner }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- platform: linux/amd64
|
||||||
|
runner: ubuntu-22.04
|
||||||
|
arch: amd64
|
||||||
|
- platform: linux/arm64
|
||||||
|
runner: ubuntu-22.04-arm
|
||||||
|
arch: arm64
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Log in to GHCR
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and push by digest
|
||||||
|
id: build
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: Dockerfile.web
|
||||||
|
platforms: ${{ matrix.platform }}
|
||||||
|
outputs: type=image,name=${{ env.IMAGE }},push-by-digest=true,name-canonical=true,push=true
|
||||||
|
|
||||||
|
- name: Export digest
|
||||||
|
run: |
|
||||||
|
mkdir -p "${{ runner.temp }}/digests"
|
||||||
|
digest="${{ steps.build.outputs.digest }}"
|
||||||
|
touch "${{ runner.temp }}/digests/${digest#sha256:}"
|
||||||
|
|
||||||
|
- name: Upload digest
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: digest-${{ matrix.arch }}
|
||||||
|
path: ${{ runner.temp }}/digests/*
|
||||||
|
if-no-files-found: error
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
|
publish:
|
||||||
|
name: Publish manifest
|
||||||
|
needs: build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Download digests
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
path: ${{ runner.temp }}/digests
|
||||||
|
pattern: digest-*
|
||||||
|
merge-multiple: true
|
||||||
|
|
||||||
|
- name: Set up Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Log in to GHCR
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
# `latest` follows a release tag, and a manual run that names a version — the way to
|
||||||
|
# publish before the first release. A prerelease (v2026.2.1-beta.1) never takes it.
|
||||||
|
- name: Tags
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ${{ env.IMAGE }}
|
||||||
|
flavor: latest=false
|
||||||
|
tags: |
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
type=raw,value=${{ inputs.version }},enable=${{ inputs.version != '' }}
|
||||||
|
type=raw,value=latest,enable=${{ inputs.version != '' || (github.event_name == 'push' && !contains(github.ref_name, '-')) }}
|
||||||
|
type=ref,event=branch
|
||||||
|
type=sha,format=short
|
||||||
|
|
||||||
|
- name: Create and push the manifest
|
||||||
|
working-directory: ${{ runner.temp }}/digests
|
||||||
|
run: |
|
||||||
|
docker buildx imagetools create \
|
||||||
|
$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||||
|
$(printf '${{ env.IMAGE }}@sha256:%s ' *)
|
||||||
|
|
||||||
|
- name: Inspect
|
||||||
|
run: docker buildx imagetools inspect ${{ env.IMAGE }}:${{ steps.meta.outputs.version }}
|
||||||
Generated
+449
-78
@@ -2,6 +2,18 @@
|
|||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 4
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "accessory"
|
||||||
|
version = "2.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cc86a613208ca7d144ca24f6ec49c5cdfa5c05a46f2f7ab5880ff99371f165de"
|
||||||
|
dependencies = [
|
||||||
|
"macroific 3.0.1",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 3.0.3",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "adler2"
|
name = "adler2"
|
||||||
version = "2.0.0"
|
version = "2.0.0"
|
||||||
@@ -40,18 +52,6 @@ dependencies = [
|
|||||||
"version_check",
|
"version_check",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ahash"
|
|
||||||
version = "0.8.12"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"once_cell",
|
|
||||||
"version_check",
|
|
||||||
"zerocopy",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aho-corasick"
|
name = "aho-corasick"
|
||||||
version = "0.6.10"
|
version = "0.6.10"
|
||||||
@@ -249,7 +249,7 @@ dependencies = [
|
|||||||
"enumflags2",
|
"enumflags2",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"rand 0.8.5",
|
"rand 0.8.7",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_repr",
|
"serde_repr",
|
||||||
"url",
|
"url",
|
||||||
@@ -265,7 +265,7 @@ dependencies = [
|
|||||||
"enumflags2",
|
"enumflags2",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"rand 0.9.1",
|
"rand 0.9.5",
|
||||||
"raw-window-handle",
|
"raw-window-handle",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_repr",
|
"serde_repr",
|
||||||
@@ -585,9 +585,9 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aws-lc-rs"
|
name = "aws-lc-rs"
|
||||||
version = "1.16.1"
|
version = "1.18.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "94bffc006df10ac2a68c83692d734a465f8ee6c5b384d8545a636f81d858f4bf"
|
checksum = "ce2b2dcc879c3bae0d371e77c99f2238400ef24ec001394befa67b6e543add9e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aws-lc-sys",
|
"aws-lc-sys",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
@@ -595,14 +595,15 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aws-lc-sys"
|
name = "aws-lc-sys"
|
||||||
version = "0.38.0"
|
version = "0.44.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4321e568ed89bb5a7d291a7f37997c2c0df89809d7b6d12062c81ddb54aa782e"
|
checksum = "f09fae7be8bb3174e05c6afdb34199e6dc0c7c04ba9fa237b1967adfbde27483"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"cmake",
|
"cmake",
|
||||||
"dunce",
|
"dunce",
|
||||||
"fs_extra",
|
"fs_extra",
|
||||||
|
"pkg-config",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -618,6 +619,8 @@ dependencies = [
|
|||||||
"http",
|
"http",
|
||||||
"http-body",
|
"http-body",
|
||||||
"http-body-util",
|
"http-body-util",
|
||||||
|
"hyper",
|
||||||
|
"hyper-util",
|
||||||
"itoa",
|
"itoa",
|
||||||
"matchit",
|
"matchit",
|
||||||
"memchr",
|
"memchr",
|
||||||
@@ -626,10 +629,15 @@ dependencies = [
|
|||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"rustversion",
|
"rustversion",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_path_to_error",
|
||||||
|
"serde_urlencoded",
|
||||||
"sync_wrapper",
|
"sync_wrapper",
|
||||||
|
"tokio",
|
||||||
"tower 0.5.2",
|
"tower 0.5.2",
|
||||||
"tower-layer",
|
"tower-layer",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -650,6 +658,7 @@ dependencies = [
|
|||||||
"sync_wrapper",
|
"sync_wrapper",
|
||||||
"tower-layer",
|
"tower-layer",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1066,10 +1075,11 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.2.26"
|
version = "1.4.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "956a5e21988b87f372569b66183b78babf23ebc2e744b733e4350a752c4dafac"
|
checksum = "509591b7bcd67f4ef775afad7662703b4935daaa6ec0e5605cfb1090b32a2b6d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"find-msvc-tools",
|
||||||
"jobserver",
|
"jobserver",
|
||||||
"libc",
|
"libc",
|
||||||
"shlex",
|
"shlex",
|
||||||
@@ -1386,6 +1396,16 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "console_error_panic_hook"
|
||||||
|
version = "0.1.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "const-random"
|
name = "const-random"
|
||||||
version = "0.1.18"
|
version = "0.1.18"
|
||||||
@@ -1870,6 +1890,20 @@ version = "0.1.9"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "da692b8d1080ea3045efaab14434d40468c3d8657e42abddfffca87b428f4c1b"
|
checksum = "da692b8d1080ea3045efaab14434d40468c3d8657e42abddfffca87b428f4c1b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "delegate-display"
|
||||||
|
version = "3.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "54e3a499943fd5180aeffcb708164ffb22125ceeccb05491b5297981d37d6249"
|
||||||
|
dependencies = [
|
||||||
|
"impartial-ord",
|
||||||
|
"itoa",
|
||||||
|
"macroific 3.0.1",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 3.0.3",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "deranged"
|
name = "deranged"
|
||||||
version = "0.5.5"
|
version = "0.5.5"
|
||||||
@@ -2361,6 +2395,18 @@ dependencies = [
|
|||||||
"regex-syntax 0.8.5",
|
"regex-syntax 0.8.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fancy_constructor"
|
||||||
|
version = "2.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4a8211ab12c36b63c269e17873331dc1da196a3edb7a5c79015677ef972da34a"
|
||||||
|
dependencies = [
|
||||||
|
"macroific 3.0.1",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 3.0.3",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fast-glob"
|
name = "fast-glob"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
@@ -2417,6 +2463,12 @@ dependencies = [
|
|||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "find-msvc-tools"
|
||||||
|
version = "0.1.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fixedbitset"
|
name = "fixedbitset"
|
||||||
version = "0.4.2"
|
version = "0.4.2"
|
||||||
@@ -2849,8 +2901,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
|
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
|
"js-sys",
|
||||||
"libc",
|
"libc",
|
||||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||||
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3091,7 +3145,7 @@ version = "0.12.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash 0.7.8",
|
"ahash",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3099,9 +3153,6 @@ name = "hashbrown"
|
|||||||
version = "0.14.5"
|
version = "0.14.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||||
dependencies = [
|
|
||||||
"ahash 0.8.12",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
@@ -3131,11 +3182,11 @@ checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashlink"
|
name = "hashlink"
|
||||||
version = "0.9.1"
|
version = "0.11.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af"
|
checksum = "824e001ac4f3012dd16a264bec811403a67ca9deb6c102fc5049b32c4574b35f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"hashbrown 0.14.5",
|
"hashbrown 0.16.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3233,6 +3284,12 @@ version = "0.1.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573"
|
checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "http-range-header"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "httparse"
|
name = "httparse"
|
||||||
version = "1.10.1"
|
version = "1.10.1"
|
||||||
@@ -3497,6 +3554,17 @@ dependencies = [
|
|||||||
"tiff",
|
"tiff",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "impartial-ord"
|
||||||
|
version = "2.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bc680e03d1a08bdc1c01b60e0c39723430ced63d8cabd93cee0449b6ba916a25"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 3.0.3",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "include_dir"
|
name = "include_dir"
|
||||||
version = "0.7.4"
|
version = "0.7.4"
|
||||||
@@ -3516,6 +3584,40 @@ dependencies = [
|
|||||||
"quote",
|
"quote",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "indexed_db_futures"
|
||||||
|
version = "0.6.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "69ff41758cbd104e91033bb53bc449bec7eea65652960c81eddf3fc146ecea19"
|
||||||
|
dependencies = [
|
||||||
|
"accessory",
|
||||||
|
"cfg-if",
|
||||||
|
"delegate-display",
|
||||||
|
"derive_more 2.1.1",
|
||||||
|
"fancy_constructor",
|
||||||
|
"indexed_db_futures_macros_internal",
|
||||||
|
"js-sys",
|
||||||
|
"sealed 0.6.0",
|
||||||
|
"smallvec",
|
||||||
|
"thiserror 2.0.17",
|
||||||
|
"tokio",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"wasm-bindgen-futures",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "indexed_db_futures_macros_internal"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "caeba94923b68f254abef921cea7e7698bf4675fdd89d7c58bf1ed885b49a27d"
|
||||||
|
dependencies = [
|
||||||
|
"macroific 2.0.0",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.101",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "1.9.3"
|
version = "1.9.3"
|
||||||
@@ -3561,17 +3663,6 @@ dependencies = [
|
|||||||
"cfb",
|
"cfb",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "inherent"
|
|
||||||
version = "1.0.12"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6c38228f24186d9cc68c729accb4d413be9eaed6ad07ff79e0270d9e56f3de13"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn 2.0.101",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "inotify"
|
name = "inotify"
|
||||||
version = "0.11.0"
|
version = "0.11.0"
|
||||||
@@ -4007,9 +4098,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libsqlite3-sys"
|
name = "libsqlite3-sys"
|
||||||
version = "0.30.1"
|
version = "0.36.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149"
|
checksum = "95b4103cffefa72eb8428cb6b47d6627161e51c2739fc5e3b734584157bc642a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"pkg-config",
|
"pkg-config",
|
||||||
@@ -4109,6 +4200,102 @@ version = "0.1.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
|
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "macroific"
|
||||||
|
version = "2.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "89f276537b4b8f981bf1c13d79470980f71134b7bdcc5e6e911e910e556b0285"
|
||||||
|
dependencies = [
|
||||||
|
"macroific_attr_parse 2.0.0",
|
||||||
|
"macroific_core 2.0.0",
|
||||||
|
"macroific_macro 2.0.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "macroific"
|
||||||
|
version = "3.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c437abb3bf40939b00d1dff858ed2050d948bc230dee39546ed16795fcf10ec2"
|
||||||
|
dependencies = [
|
||||||
|
"macroific_attr_parse 3.0.1",
|
||||||
|
"macroific_core 3.0.1",
|
||||||
|
"macroific_macro 3.0.1",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "macroific_attr_parse"
|
||||||
|
version = "2.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ad4023761b45fcd36abed8fb7ae6a80456b0a38102d55e89a57d9a594a236be9"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"sealed 0.6.0",
|
||||||
|
"syn 2.0.101",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "macroific_attr_parse"
|
||||||
|
version = "3.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "aeb7a82ec1aa16094da719be18615d79cb2574b037358e4e6c6d96eedcff3a93"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"sealed 0.7.0",
|
||||||
|
"syn 3.0.3",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "macroific_core"
|
||||||
|
version = "2.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d0a7594d3c14916fa55bef7e9d18c5daa9ed410dd37504251e4b75bbdeec33e3"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"sealed 0.6.0",
|
||||||
|
"syn 2.0.101",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "macroific_core"
|
||||||
|
version = "3.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "26c8e789484d2fa216eee9d5e0565ba80ccda389bcf0e111143a7b8f4792336e"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"sealed 0.7.0",
|
||||||
|
"syn 3.0.3",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "macroific_macro"
|
||||||
|
version = "2.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4da6f2ed796261b0a74e2b52b42c693bb6dee1effba3a482c49592659f824b3b"
|
||||||
|
dependencies = [
|
||||||
|
"macroific_attr_parse 2.0.0",
|
||||||
|
"macroific_core 2.0.0",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.101",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "macroific_macro"
|
||||||
|
version = "3.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e30d63cb102648965753f0d47a6ab171b1cbb1fb5c363f4784ea986782fc7b77"
|
||||||
|
dependencies = [
|
||||||
|
"macroific_attr_parse 3.0.1",
|
||||||
|
"macroific_core 3.0.1",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 3.0.3",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "malloc_buf"
|
name = "malloc_buf"
|
||||||
version = "0.0.6"
|
version = "0.0.6"
|
||||||
@@ -4291,7 +4478,7 @@ version = "0.4.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3ffa00dec017b5b1a8b7cf5e2c008bfda1aa7e0697ac1508b491fdf2622fb4d8"
|
checksum = "3ffa00dec017b5b1a8b7cf5e2c008bfda1aa7e0697ac1508b491fdf2622fb4d8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rand 0.8.5",
|
"rand 0.8.7",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4851,15 +5038,14 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl"
|
name = "openssl"
|
||||||
version = "0.10.73"
|
version = "0.10.81"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8"
|
checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.11.0",
|
"bitflags 2.11.0",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"foreign-types 0.3.2",
|
"foreign-types 0.3.2",
|
||||||
"libc",
|
"libc",
|
||||||
"once_cell",
|
|
||||||
"openssl-macros",
|
"openssl-macros",
|
||||||
"openssl-sys",
|
"openssl-sys",
|
||||||
]
|
]
|
||||||
@@ -4883,18 +5069,18 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl-src"
|
name = "openssl-src"
|
||||||
version = "300.5.0+3.5.0"
|
version = "300.6.1+3.6.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e8ce546f549326b0e6052b649198487d91320875da901e7bd11a06d1ee3f9c2f"
|
checksum = "46eb8fb9fb3b61ce1c0f8a026c4c1a0714d3a9e138e7fbde78753ce2babc3846"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl-sys"
|
name = "openssl-sys"
|
||||||
version = "0.9.109"
|
version = "0.9.117"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571"
|
checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"libc",
|
"libc",
|
||||||
@@ -5708,7 +5894,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6"
|
checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"phf_shared 0.10.0",
|
"phf_shared 0.10.0",
|
||||||
"rand 0.8.5",
|
"rand 0.8.7",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5718,7 +5904,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
|
checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"phf_shared 0.11.3",
|
"phf_shared 0.11.3",
|
||||||
"rand 0.8.5",
|
"rand 0.8.7",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -6242,9 +6428,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "r2d2_sqlite"
|
name = "r2d2_sqlite"
|
||||||
version = "0.25.0"
|
version = "0.32.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "eb14dba8247a6a15b7fdbc7d389e2e6f03ee9f184f87117706d509c092dfe846"
|
checksum = "a2ebd03c29250cdf191da93a35118b4567c2ef0eacab54f65e058d6f4c9965f6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"r2d2",
|
"r2d2",
|
||||||
"rusqlite",
|
"rusqlite",
|
||||||
@@ -6283,9 +6469,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand"
|
name = "rand"
|
||||||
version = "0.8.5"
|
version = "0.8.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"rand_chacha 0.3.1",
|
"rand_chacha 0.3.1",
|
||||||
@@ -6294,9 +6480,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand"
|
name = "rand"
|
||||||
version = "0.9.1"
|
version = "0.9.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
|
checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rand_chacha 0.9.0",
|
"rand_chacha 0.9.0",
|
||||||
"rand_core 0.9.3",
|
"rand_core 0.9.3",
|
||||||
@@ -7130,10 +7316,20 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rusqlite"
|
name = "rsqlite-vfs"
|
||||||
version = "0.32.1"
|
version = "0.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e"
|
checksum = "c51c9ae4df8a7fba42103df5c621fa3c37eccf3a3c650879e90fc48b11cc192c"
|
||||||
|
dependencies = [
|
||||||
|
"hashbrown 0.16.1",
|
||||||
|
"thiserror 2.0.17",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rusqlite"
|
||||||
|
version = "0.38.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f1c93dd1c9683b438c392c492109cb702b8090b2bfc8fed6f6e4eb4523f17af3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.11.0",
|
"bitflags 2.11.0",
|
||||||
"chrono",
|
"chrono",
|
||||||
@@ -7142,6 +7338,7 @@ dependencies = [
|
|||||||
"hashlink",
|
"hashlink",
|
||||||
"libsqlite3-sys",
|
"libsqlite3-sys",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
|
"sqlite-wasm-rs",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -7165,7 +7362,7 @@ dependencies = [
|
|||||||
"borsh",
|
"borsh",
|
||||||
"bytes",
|
"bytes",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
"rand 0.8.5",
|
"rand 0.8.7",
|
||||||
"rkyv",
|
"rkyv",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@@ -7423,20 +7620,20 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sea-query"
|
name = "sea-query"
|
||||||
version = "0.32.6"
|
version = "1.0.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "64c91783d1514b99754fc6a4079081dcc2c587dadbff65c48c7f62297443536a"
|
checksum = "546040c653a705e60ec65ecd3191a809603734bebbc225775916dea9ae409b31"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"inherent",
|
"itoa",
|
||||||
"sea-query-derive",
|
"sea-query-derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sea-query-derive"
|
name = "sea-query-derive"
|
||||||
version = "0.4.3"
|
version = "1.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bae0cbad6ab996955664982739354128c58d16e126114fe88c2a493642502aab"
|
checksum = "a0b0f466921cdd3cf4b89d5c3ac2173dba89a873ab395b123a645de181ec7537"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"darling 0.20.11",
|
"darling 0.20.11",
|
||||||
"heck 0.4.1",
|
"heck 0.4.1",
|
||||||
@@ -7448,9 +7645,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sea-query-rusqlite"
|
name = "sea-query-rusqlite"
|
||||||
version = "0.7.0"
|
version = "0.8.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3743bbdfb24b1a84cc1a6fbf4b1188e6851f6e00ea20944b44c56bf03a585bb4"
|
checksum = "1ec6038023c8517c623e5bf9606b3c54d40bc8296bb6b2986040428dd84deddd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rusqlite",
|
"rusqlite",
|
||||||
"sea-query",
|
"sea-query",
|
||||||
@@ -7462,6 +7659,28 @@ version = "4.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
|
checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sealed"
|
||||||
|
version = "0.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "22f968c5ea23d555e670b449c1c5e7b2fc399fdaec1d304a17cd48e288abc107"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.101",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sealed"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9b68e2ea526d9fb32f23ca8894fb5da9e743f34c2f41701f0501dc8a25c4b343"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 3.0.3",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "security-framework"
|
name = "security-framework"
|
||||||
version = "2.11.1"
|
version = "2.11.1"
|
||||||
@@ -7861,9 +8080,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "shlex"
|
name = "shlex"
|
||||||
version = "1.3.0"
|
version = "2.0.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "signal-hook"
|
name = "signal-hook"
|
||||||
@@ -8031,6 +8250,34 @@ dependencies = [
|
|||||||
"system-deps",
|
"system-deps",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sqlite-wasm-rs"
|
||||||
|
version = "0.5.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dc3efc0da82635d7e1ced0053bbbfa8c7ab9645d0bf36ceb4f7127bb85315d75"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"js-sys",
|
||||||
|
"rsqlite-vfs",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sqlite-wasm-vfs"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7f7a5c9ac229421d577bb5a9bb59048838509958b218dd4e0b3c1214a87c361e"
|
||||||
|
dependencies = [
|
||||||
|
"indexed_db_futures",
|
||||||
|
"js-sys",
|
||||||
|
"rsqlite-vfs",
|
||||||
|
"thiserror 2.0.17",
|
||||||
|
"tokio",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"wasm-bindgen-futures",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "stable_deref_trait"
|
name = "stable_deref_trait"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
@@ -8165,6 +8412,17 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "3.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sync_wrapper"
|
name = "sync_wrapper"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
@@ -9265,7 +9523,7 @@ dependencies = [
|
|||||||
"indexmap 1.9.3",
|
"indexmap 1.9.3",
|
||||||
"pin-project",
|
"pin-project",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"rand 0.8.5",
|
"rand 0.8.7",
|
||||||
"slab",
|
"slab",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
@@ -9287,6 +9545,7 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
"tower-layer",
|
"tower-layer",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -9295,12 +9554,22 @@ version = "0.6.10"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "68d6fdd9f81c2819c9a8b0e0cd91660e7746a8e6ea2ba7c6b2b057985f6bcb51"
|
checksum = "68d6fdd9f81c2819c9a8b0e0cd91660e7746a8e6ea2ba7c6b2b057985f6bcb51"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"async-compression",
|
||||||
"bitflags 2.11.0",
|
"bitflags 2.11.0",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
"futures-core",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"http",
|
"http",
|
||||||
"http-body",
|
"http-body",
|
||||||
|
"http-body-util",
|
||||||
|
"http-range-header",
|
||||||
|
"httpdate",
|
||||||
|
"mime",
|
||||||
|
"mime_guess",
|
||||||
|
"percent-encoding",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
|
"tokio",
|
||||||
|
"tokio-util",
|
||||||
"tower 0.5.2",
|
"tower 0.5.2",
|
||||||
"tower-layer",
|
"tower-layer",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
@@ -9325,6 +9594,7 @@ version = "0.1.41"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
|
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"log 0.4.29",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"tracing-attributes",
|
"tracing-attributes",
|
||||||
"tracing-core",
|
"tracing-core",
|
||||||
@@ -9482,7 +9752,7 @@ dependencies = [
|
|||||||
"http",
|
"http",
|
||||||
"httparse",
|
"httparse",
|
||||||
"log 0.4.29",
|
"log 0.4.29",
|
||||||
"rand 0.9.1",
|
"rand 0.9.5",
|
||||||
"rustls",
|
"rustls",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
"sha1",
|
"sha1",
|
||||||
@@ -9752,7 +10022,7 @@ checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom 0.3.3",
|
"getrandom 0.3.3",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"rand 0.9.1",
|
"rand 0.9.5",
|
||||||
"serde",
|
"serde",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
@@ -10952,6 +11222,7 @@ name = "yaak"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
|
"base64 0.22.1",
|
||||||
"log 0.4.29",
|
"log 0.4.29",
|
||||||
"md5 0.8.0",
|
"md5 0.8.0",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@@ -10996,7 +11267,7 @@ dependencies = [
|
|||||||
"pretty_graphql",
|
"pretty_graphql",
|
||||||
"r2d2",
|
"r2d2",
|
||||||
"r2d2_sqlite",
|
"r2d2_sqlite",
|
||||||
"rand 0.9.1",
|
"rand 0.9.5",
|
||||||
"reqwest 0.12.20",
|
"reqwest 0.12.20",
|
||||||
"rlimit",
|
"rlimit",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -11023,6 +11294,7 @@ dependencies = [
|
|||||||
"uuid",
|
"uuid",
|
||||||
"yaak",
|
"yaak",
|
||||||
"yaak-api",
|
"yaak-api",
|
||||||
|
"yaak-commands",
|
||||||
"yaak-common",
|
"yaak-common",
|
||||||
"yaak-core",
|
"yaak-core",
|
||||||
"yaak-crypto",
|
"yaak-crypto",
|
||||||
@@ -11031,9 +11303,12 @@ dependencies = [
|
|||||||
"yaak-grpc",
|
"yaak-grpc",
|
||||||
"yaak-http",
|
"yaak-http",
|
||||||
"yaak-license",
|
"yaak-license",
|
||||||
|
"yaak-lifecycle",
|
||||||
"yaak-mac-window",
|
"yaak-mac-window",
|
||||||
"yaak-models",
|
"yaak-models",
|
||||||
"yaak-plugins",
|
"yaak-plugins",
|
||||||
|
"yaak-rpc",
|
||||||
|
"yaak-rpc-schema",
|
||||||
"yaak-sse",
|
"yaak-sse",
|
||||||
"yaak-sync",
|
"yaak-sync",
|
||||||
"yaak-system-appearance",
|
"yaak-system-appearance",
|
||||||
@@ -11078,7 +11353,7 @@ dependencies = [
|
|||||||
"log 0.4.29",
|
"log 0.4.29",
|
||||||
"oxc_resolver",
|
"oxc_resolver",
|
||||||
"predicates",
|
"predicates",
|
||||||
"rand 0.8.5",
|
"rand 0.8.7",
|
||||||
"reqwest 0.12.20",
|
"reqwest 0.12.20",
|
||||||
"rolldown",
|
"rolldown",
|
||||||
"schemars 0.8.22",
|
"schemars 0.8.22",
|
||||||
@@ -11094,12 +11369,30 @@ dependencies = [
|
|||||||
"yaak-core",
|
"yaak-core",
|
||||||
"yaak-crypto",
|
"yaak-crypto",
|
||||||
"yaak-http",
|
"yaak-http",
|
||||||
|
"yaak-lifecycle",
|
||||||
"yaak-models",
|
"yaak-models",
|
||||||
"yaak-plugins",
|
"yaak-plugins",
|
||||||
"yaak-templates",
|
"yaak-templates",
|
||||||
"zip",
|
"zip",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "yaak-commands"
|
||||||
|
version = "0.0.0"
|
||||||
|
dependencies = [
|
||||||
|
"serde_json",
|
||||||
|
"tempfile",
|
||||||
|
"thiserror 2.0.17",
|
||||||
|
"tokio",
|
||||||
|
"yaak",
|
||||||
|
"yaak-core",
|
||||||
|
"yaak-crypto",
|
||||||
|
"yaak-models",
|
||||||
|
"yaak-plugins",
|
||||||
|
"yaak-rpc-schema",
|
||||||
|
"yaak-templates",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yaak-common"
|
name = "yaak-common"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -11134,6 +11427,7 @@ name = "yaak-database"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
|
"getrandom 0.2.16",
|
||||||
"include_dir",
|
"include_dir",
|
||||||
"log 0.4.29",
|
"log 0.4.29",
|
||||||
"nanoid",
|
"nanoid",
|
||||||
@@ -11146,6 +11440,7 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"thiserror 2.0.17",
|
"thiserror 2.0.17",
|
||||||
"ts-rs",
|
"ts-rs",
|
||||||
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -11201,6 +11496,7 @@ dependencies = [
|
|||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
"tonic",
|
"tonic",
|
||||||
"tonic-reflection",
|
"tonic-reflection",
|
||||||
|
"ts-rs",
|
||||||
"uuid",
|
"uuid",
|
||||||
"yaak-common",
|
"yaak-common",
|
||||||
"yaak-tls",
|
"yaak-tls",
|
||||||
@@ -11222,10 +11518,10 @@ dependencies = [
|
|||||||
"log 0.4.29",
|
"log 0.4.29",
|
||||||
"mime_guess",
|
"mime_guess",
|
||||||
"native-tls",
|
"native-tls",
|
||||||
"regex 1.11.1",
|
|
||||||
"reqwest 0.12.20",
|
"reqwest 0.12.20",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"tempfile",
|
||||||
"thiserror 2.0.17",
|
"thiserror 2.0.17",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
@@ -11257,6 +11553,14 @@ dependencies = [
|
|||||||
"yaak-models",
|
"yaak-models",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "yaak-lifecycle"
|
||||||
|
version = "0.0.0"
|
||||||
|
dependencies = [
|
||||||
|
"log 0.4.29",
|
||||||
|
"yaak-models",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yaak-mac-window"
|
name = "yaak-mac-window"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -11265,7 +11569,7 @@ dependencies = [
|
|||||||
"csscolorparser",
|
"csscolorparser",
|
||||||
"log 0.4.29",
|
"log 0.4.29",
|
||||||
"objc",
|
"objc",
|
||||||
"rand 0.9.1",
|
"rand 0.9.5",
|
||||||
"tauri",
|
"tauri",
|
||||||
"tauri-plugin",
|
"tauri-plugin",
|
||||||
]
|
]
|
||||||
@@ -11290,8 +11594,10 @@ dependencies = [
|
|||||||
"sha2",
|
"sha2",
|
||||||
"thiserror 2.0.17",
|
"thiserror 2.0.17",
|
||||||
"ts-rs",
|
"ts-rs",
|
||||||
|
"urlencoding",
|
||||||
"yaak-core",
|
"yaak-core",
|
||||||
"yaak-database",
|
"yaak-database",
|
||||||
|
"yaak-templates",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -11307,7 +11613,7 @@ dependencies = [
|
|||||||
"log 0.4.29",
|
"log 0.4.29",
|
||||||
"md5 0.7.0",
|
"md5 0.7.0",
|
||||||
"path-slash",
|
"path-slash",
|
||||||
"rand 0.9.1",
|
"rand 0.9.5",
|
||||||
"reqwest 0.12.20",
|
"reqwest 0.12.20",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@@ -11369,6 +11675,22 @@ dependencies = [
|
|||||||
"ts-rs",
|
"ts-rs",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "yaak-rpc-schema"
|
||||||
|
version = "0.0.0"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"ts-rs",
|
||||||
|
"yaak-git",
|
||||||
|
"yaak-grpc",
|
||||||
|
"yaak-models",
|
||||||
|
"yaak-plugins",
|
||||||
|
"yaak-sse",
|
||||||
|
"yaak-sync",
|
||||||
|
"yaak-templates",
|
||||||
|
"yaak-ws",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yaak-sse"
|
name = "yaak-sse"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -11401,7 +11723,10 @@ name = "yaak-system-appearance"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"dark-light",
|
"dark-light",
|
||||||
|
"dispatch2",
|
||||||
"log 0.4.29",
|
"log 0.4.29",
|
||||||
|
"objc2-app-kit",
|
||||||
|
"objc2-foundation 0.3.1",
|
||||||
"tauri",
|
"tauri",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -11411,6 +11736,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"regex 1.11.1",
|
"regex 1.11.1",
|
||||||
"tauri",
|
"tauri",
|
||||||
|
"yaak-core",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -11434,6 +11760,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"log 0.4.29",
|
"log 0.4.29",
|
||||||
"p12",
|
"p12",
|
||||||
|
"pem",
|
||||||
"rustls",
|
"rustls",
|
||||||
"rustls-pemfile",
|
"rustls-pemfile",
|
||||||
"rustls-platform-verifier",
|
"rustls-platform-verifier",
|
||||||
@@ -11441,6 +11768,50 @@ dependencies = [
|
|||||||
"thiserror 2.0.17",
|
"thiserror 2.0.17",
|
||||||
"url",
|
"url",
|
||||||
"yaak-models",
|
"yaak-models",
|
||||||
|
"yasna",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "yaak-wasm"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"console_error_panic_hook",
|
||||||
|
"js-sys",
|
||||||
|
"log 0.4.29",
|
||||||
|
"serde",
|
||||||
|
"serde-wasm-bindgen",
|
||||||
|
"serde_json",
|
||||||
|
"sqlite-wasm-rs",
|
||||||
|
"sqlite-wasm-vfs",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"wasm-bindgen-futures",
|
||||||
|
"web-sys",
|
||||||
|
"yaak-lifecycle",
|
||||||
|
"yaak-models",
|
||||||
|
"yaak-templates",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "yaak-web"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
|
"axum",
|
||||||
|
"base64 0.22.1",
|
||||||
|
"bytes",
|
||||||
|
"clap",
|
||||||
|
"env_logger",
|
||||||
|
"futures-util",
|
||||||
|
"log 0.4.29",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tokio",
|
||||||
|
"tower-http",
|
||||||
|
"ts-rs",
|
||||||
|
"url",
|
||||||
|
"uuid",
|
||||||
|
"yaak-http",
|
||||||
|
"yaak-models",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -11449,7 +11820,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"log 0.4.29",
|
"log 0.4.29",
|
||||||
"md5 0.8.0",
|
"md5 0.8.0",
|
||||||
"rand 0.9.1",
|
"rand 0.9.5",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tauri",
|
"tauri",
|
||||||
|
|||||||
@@ -2,9 +2,11 @@
|
|||||||
resolver = "2"
|
resolver = "2"
|
||||||
members = [
|
members = [
|
||||||
"crates/yaak",
|
"crates/yaak",
|
||||||
|
"crates/yaak-commands",
|
||||||
# Common/foundation crates
|
# Common/foundation crates
|
||||||
"crates/common/yaak-database",
|
"crates/common/yaak-database",
|
||||||
"crates/common/yaak-rpc",
|
"crates/common/yaak-rpc",
|
||||||
|
"crates/common/yaak-rpc-schema",
|
||||||
# Shared crates (no Tauri dependency)
|
# Shared crates (no Tauri dependency)
|
||||||
"crates/yaak-core",
|
"crates/yaak-core",
|
||||||
"crates/yaak-common",
|
"crates/yaak-common",
|
||||||
@@ -12,6 +14,7 @@ members = [
|
|||||||
"crates/yaak-git",
|
"crates/yaak-git",
|
||||||
"crates/yaak-grpc",
|
"crates/yaak-grpc",
|
||||||
"crates/yaak-http",
|
"crates/yaak-http",
|
||||||
|
"crates/yaak-lifecycle",
|
||||||
"crates/yaak-models",
|
"crates/yaak-models",
|
||||||
"crates/yaak-plugins",
|
"crates/yaak-plugins",
|
||||||
"crates/yaak-sse",
|
"crates/yaak-sse",
|
||||||
@@ -19,10 +22,13 @@ members = [
|
|||||||
"crates/yaak-templates",
|
"crates/yaak-templates",
|
||||||
"crates/yaak-tls",
|
"crates/yaak-tls",
|
||||||
"crates/yaak-ws",
|
"crates/yaak-ws",
|
||||||
|
"crates/yaak-wasm",
|
||||||
"crates/yaak-api",
|
"crates/yaak-api",
|
||||||
"crates/yaak-proxy",
|
"crates/yaak-proxy",
|
||||||
# Proxy-specific crates
|
# Proxy-specific crates
|
||||||
"crates-proxy/yaak-proxy-lib",
|
"crates-proxy/yaak-proxy-lib",
|
||||||
|
# Server crates (the browser tier's hosted send executor)
|
||||||
|
"crates-server/yaak-web",
|
||||||
# CLI crates
|
# CLI crates
|
||||||
"crates-cli/yaak-cli",
|
"crates-cli/yaak-cli",
|
||||||
# Tauri-specific crates
|
# Tauri-specific crates
|
||||||
@@ -63,15 +69,18 @@ ts-rs = "11.1.0"
|
|||||||
# Internal crates - common/foundation
|
# Internal crates - common/foundation
|
||||||
yaak-database = { path = "crates/common/yaak-database" }
|
yaak-database = { path = "crates/common/yaak-database" }
|
||||||
yaak-rpc = { path = "crates/common/yaak-rpc" }
|
yaak-rpc = { path = "crates/common/yaak-rpc" }
|
||||||
|
yaak-rpc-schema = { path = "crates/common/yaak-rpc-schema" }
|
||||||
|
|
||||||
# Internal crates - shared
|
# Internal crates - shared
|
||||||
yaak-core = { path = "crates/yaak-core" }
|
yaak-core = { path = "crates/yaak-core" }
|
||||||
yaak = { path = "crates/yaak" }
|
yaak = { path = "crates/yaak" }
|
||||||
|
yaak-commands = { path = "crates/yaak-commands" }
|
||||||
yaak-common = { path = "crates/yaak-common" }
|
yaak-common = { path = "crates/yaak-common" }
|
||||||
yaak-crypto = { path = "crates/yaak-crypto" }
|
yaak-crypto = { path = "crates/yaak-crypto" }
|
||||||
yaak-git = { path = "crates/yaak-git" }
|
yaak-git = { path = "crates/yaak-git" }
|
||||||
yaak-grpc = { path = "crates/yaak-grpc" }
|
yaak-grpc = { path = "crates/yaak-grpc" }
|
||||||
yaak-http = { path = "crates/yaak-http" }
|
yaak-http = { path = "crates/yaak-http" }
|
||||||
|
yaak-lifecycle = { path = "crates/yaak-lifecycle" }
|
||||||
yaak-models = { path = "crates/yaak-models" }
|
yaak-models = { path = "crates/yaak-models" }
|
||||||
yaak-plugins = { path = "crates/yaak-plugins" }
|
yaak-plugins = { path = "crates/yaak-plugins" }
|
||||||
yaak-sse = { path = "crates/yaak-sse" }
|
yaak-sse = { path = "crates/yaak-sse" }
|
||||||
|
|||||||
@@ -44,6 +44,25 @@ After bootstrapping, start the app in development mode:
|
|||||||
npm start
|
npm start
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Run the App in a Browser
|
||||||
|
|
||||||
|
The client can also run as a plain web page, with no Tauri and no local process
|
||||||
|
behind it. Set `YAAK_TARGET=web` and start the frontend on its own:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
YAAK_TARGET=web npm run dev --workspace @yaakapp/yaak-client
|
||||||
|
```
|
||||||
|
|
||||||
|
That flag picks the browser host in `packages/platform/src/web/`, which answers
|
||||||
|
commands from an IndexedDB database the page owns instead of from the Rust
|
||||||
|
engine. Data persists across reloads and is shared between tabs on the same
|
||||||
|
origin. Sending HTTP is not available yet — the Send button reports that and
|
||||||
|
everything else about the request is still saved. `packages/platform/src/web/README.md`
|
||||||
|
lists which commands the browser host implements and which it declines.
|
||||||
|
|
||||||
|
Desktop builds are unaffected: without the flag the platform package installs
|
||||||
|
the Tauri host exactly as before.
|
||||||
|
|
||||||
## SQLite Migrations
|
## SQLite Migrations
|
||||||
|
|
||||||
New migrations can be created from the `src-tauri/` directory:
|
New migrations can be created from the `src-tauri/` directory:
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# syntax=docker/dockerfile:1
|
||||||
|
# Yaak in a browser, whole: the web client and the server that executes its sends, in one
|
||||||
|
# image serving both from one origin.
|
||||||
|
#
|
||||||
|
# docker run -p 8080:8080 ghcr.io/mountain-loop/yaak-web
|
||||||
|
#
|
||||||
|
# See crates-server/yaak-web/README.md for the knobs.
|
||||||
|
|
||||||
|
FROM node:22-slim AS web
|
||||||
|
WORKDIR /app
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
git python3 make g++ ca-certificates && rm -rf /var/lib/apt/lists/*
|
||||||
|
COPY . .
|
||||||
|
# `npm ci` runs a prepare hook (`vp config`) that shells out to git, and there is no .git in
|
||||||
|
# the build context — it is ignored, and in a worktree it is a pointer file anyway.
|
||||||
|
RUN git init -q && git add -A \
|
||||||
|
&& git -c user.email=build@yaak.app -c user.name=build commit -qm build
|
||||||
|
# Empty means the tab posts sends to its own origin, which is what this image serves. Set it
|
||||||
|
# only to build a bundle for a deployment whose server lives somewhere else.
|
||||||
|
ARG VITE_YAAK_WEB_URL=""
|
||||||
|
ENV VITE_YAAK_WEB_URL=$VITE_YAAK_WEB_URL
|
||||||
|
ENV YAAK_TARGET=web
|
||||||
|
# crates/yaak-wasm's wasm package is committed; rebuilding it needs a clang with a WebAssembly
|
||||||
|
# backend, which this image has no reason to carry.
|
||||||
|
ENV SKIP_WASM_BUILD=1
|
||||||
|
RUN npm ci
|
||||||
|
RUN node_modules/.bin/vp -C apps/yaak-client build
|
||||||
|
|
||||||
|
FROM rust:1-bookworm AS server
|
||||||
|
WORKDIR /app
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
pkg-config libssl-dev protobuf-compiler && rm -rf /var/lib/apt/lists/*
|
||||||
|
COPY . .
|
||||||
|
RUN cargo build --release -p yaak-web
|
||||||
|
|
||||||
|
FROM debian:bookworm-slim
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
ca-certificates libssl3 && rm -rf /var/lib/apt/lists/*
|
||||||
|
COPY --from=server /app/target/release/yaak-web /usr/local/bin/yaak-web
|
||||||
|
COPY --from=web /app/dist/apps/yaak-client /srv
|
||||||
|
ENV YAAK_WEB_BIND=0.0.0.0:8080
|
||||||
|
EXPOSE 8080
|
||||||
|
USER nobody
|
||||||
|
# Overriding the command (dropping --serve) leaves the stateless send executor:
|
||||||
|
# docker run ghcr.io/mountain-loop/yaak-web yaak-web
|
||||||
|
CMD ["yaak-web", "--serve", "/srv"]
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import type { HttpRequest } from "@yaakapp-internal/models";
|
||||||
|
import { patchModelById } from "@yaakapp-internal/models";
|
||||||
|
import { activeWorkspaceIdAtom } from "../hooks/useActiveWorkspace";
|
||||||
|
import { createFastMutation } from "../hooks/useFastMutation";
|
||||||
|
import { wasUpdatedExternally } from "../hooks/useRequestUpdateKey";
|
||||||
|
import { createRequestAndNavigate } from "../lib/createRequestAndNavigate";
|
||||||
|
import { jotaiStore } from "../lib/jotai";
|
||||||
|
import { rpc } from "../lib/rpc";
|
||||||
|
import { showToast } from "../lib/toast";
|
||||||
|
|
||||||
|
export function looksLikeCurl(text: string) {
|
||||||
|
return text.trim().startsWith("curl ");
|
||||||
|
}
|
||||||
|
|
||||||
|
export const importCurl = createFastMutation<
|
||||||
|
void,
|
||||||
|
string,
|
||||||
|
{ overwriteRequestId?: string; command: string }
|
||||||
|
>({
|
||||||
|
mutationKey: ["import_curl"],
|
||||||
|
mutationFn: async ({ overwriteRequestId, command }) => {
|
||||||
|
const workspaceId = jotaiStore.get(activeWorkspaceIdAtom);
|
||||||
|
const importedRequest: HttpRequest = await rpc("cmd_curl_to_request", {
|
||||||
|
command,
|
||||||
|
workspaceId,
|
||||||
|
});
|
||||||
|
|
||||||
|
let verb: string;
|
||||||
|
if (overwriteRequestId == null) {
|
||||||
|
verb = "Created";
|
||||||
|
await createRequestAndNavigate(importedRequest);
|
||||||
|
} else {
|
||||||
|
verb = "Updated";
|
||||||
|
await patchModelById(importedRequest.model, overwriteRequestId, (r: HttpRequest) => ({
|
||||||
|
...importedRequest,
|
||||||
|
id: r.id,
|
||||||
|
createdAt: r.createdAt,
|
||||||
|
workspaceId: r.workspaceId,
|
||||||
|
folderId: r.folderId,
|
||||||
|
name: r.name,
|
||||||
|
sortPriority: r.sortPriority,
|
||||||
|
}));
|
||||||
|
|
||||||
|
setTimeout(() => wasUpdatedExternally(overwriteRequestId), 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
showToast({
|
||||||
|
color: "success",
|
||||||
|
message: `${verb} request from Curl`,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -1,26 +1,44 @@
|
|||||||
import type { SettingsTab } from "../components/Settings/Settings";
|
import { platform } from "@yaakapp-internal/platform";
|
||||||
|
import type { SettingsTab, SettingsTabWithSubtab } from "../components/Settings/Settings";
|
||||||
import { activeWorkspaceIdAtom } from "../hooks/useActiveWorkspace";
|
import { activeWorkspaceIdAtom } from "../hooks/useActiveWorkspace";
|
||||||
import { createFastMutation } from "../hooks/useFastMutation";
|
import { createFastMutation } from "../hooks/useFastMutation";
|
||||||
|
import { showDialog } from "../lib/dialog";
|
||||||
import { jotaiStore } from "../lib/jotai";
|
import { jotaiStore } from "../lib/jotai";
|
||||||
import { router } from "../lib/router";
|
import { router } from "../lib/router";
|
||||||
import { invokeCmd } from "../lib/tauri";
|
import { rpc } from "../lib/rpc";
|
||||||
|
|
||||||
// Allow tab with optional subtab (e.g., "plugins:installed")
|
export const openSettings = createFastMutation<void, string, SettingsTabWithSubtab | null>({
|
||||||
type SettingsTabWithSubtab = SettingsTab | `${SettingsTab}:${string}` | null;
|
|
||||||
|
|
||||||
export const openSettings = createFastMutation<void, string, SettingsTabWithSubtab>({
|
|
||||||
mutationKey: ["open_settings"],
|
mutationKey: ["open_settings"],
|
||||||
mutationFn: async (tab) => {
|
mutationFn: async (tab) => {
|
||||||
const workspaceId = jotaiStore.get(activeWorkspaceIdAtom);
|
const workspaceId = jotaiStore.get(activeWorkspaceIdAtom);
|
||||||
if (workspaceId == null) return;
|
if (workspaceId == null) return;
|
||||||
|
|
||||||
|
// Settings is its own window where the host has windows to give. Where it
|
||||||
|
// doesn't — a browser tab — it's a dialog like any other, so opening it
|
||||||
|
// doesn't take you away from the request you were working on.
|
||||||
|
if (!platform.capabilities.multiWindow) {
|
||||||
|
// Imported here so Settings stays out of the startup bundle, the way the
|
||||||
|
// route that renders it on desktop already keeps it
|
||||||
|
const { default: Settings } = await import("../components/Settings/Settings");
|
||||||
|
showDialog({
|
||||||
|
id: "settings",
|
||||||
|
size: "md",
|
||||||
|
className: "h-[calc(100vh-5rem)] max-h-150! overflow-hidden",
|
||||||
|
noPadding: true,
|
||||||
|
noScroll: true,
|
||||||
|
// Keyed so opening a specific tab while the dialog is already up moves to it
|
||||||
|
render: ({ hide }) => <Settings key={tab ?? "general"} tab={tab} hide={hide} />,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const location = router.buildLocation({
|
const location = router.buildLocation({
|
||||||
to: "/workspaces/$workspaceId/settings",
|
to: "/workspaces/$workspaceId/settings",
|
||||||
params: { workspaceId },
|
params: { workspaceId },
|
||||||
search: { tab: (tab ?? undefined) as SettingsTab | undefined },
|
search: { tab: (tab ?? undefined) as SettingsTab | undefined },
|
||||||
});
|
});
|
||||||
|
|
||||||
await invokeCmd("cmd_new_child_window", {
|
await rpc("cmd_new_child_window", {
|
||||||
url: location.href,
|
url: location.href,
|
||||||
label: "settings",
|
label: "settings",
|
||||||
title: "Yaak Settings",
|
title: "Yaak Settings",
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
|
import { platform } from "@yaakapp-internal/platform";
|
||||||
import { createFastMutation } from "../hooks/useFastMutation";
|
import { createFastMutation } from "../hooks/useFastMutation";
|
||||||
import { getRecentCookieJars } from "../hooks/useRecentCookieJars";
|
import { getRecentCookieJars } from "../hooks/useRecentCookieJars";
|
||||||
import { getRecentEnvironments } from "../hooks/useRecentEnvironments";
|
import { getRecentEnvironments } from "../hooks/useRecentEnvironments";
|
||||||
import { getRecentRequests } from "../hooks/useRecentRequests";
|
import { getRecentRequests } from "../hooks/useRecentRequests";
|
||||||
import { router } from "../lib/router";
|
import { router } from "../lib/router";
|
||||||
import { invokeCmd } from "../lib/tauri";
|
import { rpc } from "../lib/rpc";
|
||||||
|
|
||||||
export const switchWorkspace = createFastMutation<
|
export const switchWorkspace = createFastMutation<
|
||||||
void,
|
void,
|
||||||
@@ -24,13 +25,15 @@ export const switchWorkspace = createFastMutation<
|
|||||||
request_id: requestId,
|
request_id: requestId,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (inNewWindow) {
|
// A host without windows opens the workspace here instead. Refusing would
|
||||||
|
// strand the user on the workspace they were trying to leave.
|
||||||
|
if (inNewWindow && platform.capabilities.multiWindow) {
|
||||||
const location = router.buildLocation({
|
const location = router.buildLocation({
|
||||||
to: "/workspaces/$workspaceId",
|
to: "/workspaces/$workspaceId",
|
||||||
params: { workspaceId },
|
params: { workspaceId },
|
||||||
search,
|
search,
|
||||||
});
|
});
|
||||||
await invokeCmd<void>("cmd_new_main_window", { url: location.href });
|
await rpc<void>("cmd_new_main_window", { url: location.href });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { open } from "@tauri-apps/plugin-dialog";
|
|
||||||
import { gitClone } from "@yaakapp-internal/git";
|
import { gitClone } from "@yaakapp-internal/git";
|
||||||
import { Banner, VStack } from "@yaakapp-internal/ui";
|
import { Banner, VStack } from "@yaakapp-internal/ui";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
@@ -11,6 +10,7 @@ import { Checkbox } from "./core/Checkbox";
|
|||||||
import { IconButton } from "./core/IconButton";
|
import { IconButton } from "./core/IconButton";
|
||||||
import { PlainInput } from "./core/PlainInput";
|
import { PlainInput } from "./core/PlainInput";
|
||||||
import { promptCredentials } from "./git/credentials";
|
import { promptCredentials } from "./git/credentials";
|
||||||
|
import { platform } from "@yaakapp-internal/platform";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
hide: () => void;
|
hide: () => void;
|
||||||
@@ -38,7 +38,7 @@ export function CloneGitRepositoryDialog({ hide }: Props) {
|
|||||||
hasSubdirectory && subdirectory ? `${directory}${sep}${subdirectory}` : directory;
|
hasSubdirectory && subdirectory ? `${directory}${sep}${subdirectory}` : directory;
|
||||||
|
|
||||||
const handleSelectDirectory = async () => {
|
const handleSelectDirectory = async () => {
|
||||||
const dir = await open({
|
const dir = await platform.dialog.open({
|
||||||
title: "Select Directory",
|
title: "Select Directory",
|
||||||
directory: true,
|
directory: true,
|
||||||
multiple: false,
|
multiple: false,
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { invoke } from "@tauri-apps/api/core";
|
import { checkLicense } from "@yaakapp-internal/license";
|
||||||
import { openUrl } from "@tauri-apps/plugin-opener";
|
|
||||||
import type { LicenseCheckStatus } from "@yaakapp-internal/license";
|
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { useKeyValue } from "../hooks/useKeyValue";
|
import { useKeyValue } from "../hooks/useKeyValue";
|
||||||
import { appInfo } from "../lib/appInfo";
|
import { appInfo } from "../lib/appInfo";
|
||||||
import { pricingUrl } from "../lib/pricingUrl";
|
import { pricingUrl } from "../lib/pricingUrl";
|
||||||
import { DismissibleBanner } from "./core/DismissibleBanner";
|
import { DismissibleBanner } from "./core/DismissibleBanner";
|
||||||
|
import { platform } from "@yaakapp-internal/platform";
|
||||||
|
|
||||||
const COMMERCIAL_USE_SNOOZE_MS = 7 * 24 * 60 * 60 * 1000;
|
const COMMERCIAL_USE_SNOOZE_MS = 7 * 24 * 60 * 60 * 1000;
|
||||||
const COMMERCIAL_USE_BANNER_MESSAGE =
|
const COMMERCIAL_USE_BANNER_MESSAGE =
|
||||||
@@ -95,7 +94,7 @@ async function shouldShowCommercialUsePrompt(): Promise<boolean> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const license = await invoke<LicenseCheckStatus>("plugin:yaak-license|check");
|
const license = await checkLicense();
|
||||||
return license.status === "personal_use";
|
return license.status === "personal_use";
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log("Failed to check license before commercial-use prompt", err);
|
console.log("Failed to check license before commercial-use prompt", err);
|
||||||
@@ -104,7 +103,7 @@ async function shouldShowCommercialUsePrompt(): Promise<boolean> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function openCommercialUsePricing(source: string): Promise<void> {
|
async function openCommercialUsePricing(source: string): Promise<void> {
|
||||||
await openUrl(pricingUrl(`app.commercial-use.${source}`)).catch(console.error);
|
await platform.openUrl(pricingUrl(`app.commercial-use.${source}`)).catch(console.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isSnoozed(value: string | null, ms: number): boolean {
|
function isSnoozed(value: string | null, ms: number): boolean {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { VStack } from "@yaakapp-internal/ui";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { router } from "../lib/router";
|
import { router } from "../lib/router";
|
||||||
import { setupOrConfigureEncryption } from "../lib/setupOrConfigureEncryption";
|
import { setupOrConfigureEncryption } from "../lib/setupOrConfigureEncryption";
|
||||||
import { invokeCmd } from "../lib/tauri";
|
import { rpc } from "../lib/rpc";
|
||||||
import { showErrorToast } from "../lib/toast";
|
import { showErrorToast } from "../lib/toast";
|
||||||
import { Button } from "./core/Button";
|
import { Button } from "./core/Button";
|
||||||
import { Checkbox } from "./core/Checkbox";
|
import { Checkbox } from "./core/Checkbox";
|
||||||
@@ -39,7 +39,7 @@ export function CreateWorkspaceDialog({ hide }: Props) {
|
|||||||
|
|
||||||
// Do getWorkspaceMeta instead of naively creating one because it might have
|
// Do getWorkspaceMeta instead of naively creating one because it might have
|
||||||
// been created already when the store refreshes the workspace meta after
|
// been created already when the store refreshes the workspace meta after
|
||||||
const workspaceMeta = await invokeCmd<WorkspaceMeta>("cmd_get_workspace_meta", {
|
const workspaceMeta = await rpc<WorkspaceMeta>("cmd_get_workspace_meta", {
|
||||||
workspaceId,
|
workspaceId,
|
||||||
});
|
});
|
||||||
await updateModel({
|
await updateModel({
|
||||||
|
|||||||
@@ -67,7 +67,14 @@ export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdo
|
|||||||
)}
|
)}
|
||||||
// If no environments, the button simply opens the dialog.
|
// If no environments, the button simply opens the dialog.
|
||||||
// NOTE: We don't create a new button because we want to reuse the hotkey from the menu items
|
// NOTE: We don't create a new button because we want to reuse the hotkey from the menu items
|
||||||
onClick={subEnvironments.length === 0 ? () => editEnvironment(null) : undefined}
|
onClick={
|
||||||
|
subEnvironments.length === 0
|
||||||
|
? (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
editEnvironment(null);
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
{...buttonProps}
|
{...buttonProps}
|
||||||
>
|
>
|
||||||
<EnvironmentColorIndicator environment={activeEnvironment ?? null} />
|
<EnvironmentColorIndicator environment={activeEnvironment ?? null} />
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { save } from "@tauri-apps/plugin-dialog";
|
|
||||||
import type { Workspace } from "@yaakapp-internal/models";
|
import type { Workspace } from "@yaakapp-internal/models";
|
||||||
import { workspacesAtom } from "@yaakapp-internal/models";
|
import { workspacesAtom } from "@yaakapp-internal/models";
|
||||||
import { HStack, VStack } from "@yaakapp-internal/ui";
|
import { HStack, VStack } from "@yaakapp-internal/ui";
|
||||||
@@ -7,12 +6,13 @@ import { useCallback, useMemo, useState } from "react";
|
|||||||
import slugify from "slugify";
|
import slugify from "slugify";
|
||||||
import { activeWorkspaceAtom } from "../hooks/useActiveWorkspace";
|
import { activeWorkspaceAtom } from "../hooks/useActiveWorkspace";
|
||||||
import { pluralizeCount } from "../lib/pluralize";
|
import { pluralizeCount } from "../lib/pluralize";
|
||||||
import { invokeCmd } from "../lib/tauri";
|
import { rpc } from "../lib/rpc";
|
||||||
import { CommercialUseBanner } from "./CommercialUseBanner";
|
import { CommercialUseBanner } from "./CommercialUseBanner";
|
||||||
import { Button } from "./core/Button";
|
import { Button } from "./core/Button";
|
||||||
import { Checkbox } from "./core/Checkbox";
|
import { Checkbox } from "./core/Checkbox";
|
||||||
import { DetailsBanner } from "./core/DetailsBanner";
|
import { DetailsBanner } from "./core/DetailsBanner";
|
||||||
import { Link } from "./core/Link";
|
import { Link } from "./core/Link";
|
||||||
|
import { platform } from "@yaakapp-internal/platform";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onHide: () => void;
|
onHide: () => void;
|
||||||
@@ -65,7 +65,7 @@ function ExportDataDialogContent({
|
|||||||
const ids = Object.keys(selectedWorkspaces).filter((k) => selectedWorkspaces[k]);
|
const ids = Object.keys(selectedWorkspaces).filter((k) => selectedWorkspaces[k]);
|
||||||
const workspace = ids.length === 1 ? workspaces.find((w) => w.id === ids[0]) : undefined;
|
const workspace = ids.length === 1 ? workspaces.find((w) => w.id === ids[0]) : undefined;
|
||||||
const slug = workspace ? slugify(workspace.name, { lower: true }) : "workspaces";
|
const slug = workspace ? slugify(workspace.name, { lower: true }) : "workspaces";
|
||||||
const exportPath = await save({
|
const exportPath = await platform.dialog.save({
|
||||||
title: "Export Data",
|
title: "Export Data",
|
||||||
defaultPath: `yaak.${slug}.json`,
|
defaultPath: `yaak.${slug}.json`,
|
||||||
});
|
});
|
||||||
@@ -73,7 +73,7 @@ function ExportDataDialogContent({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await invokeCmd("cmd_export_data", {
|
await rpc("cmd_export_data", {
|
||||||
workspaceIds: ids,
|
workspaceIds: ids,
|
||||||
exportPath,
|
exportPath,
|
||||||
includePrivateEnvironments: includePrivateEnvironments,
|
includePrivateEnvironments: includePrivateEnvironments,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { HStack, VStack } from "@yaakapp-internal/ui";
|
|||||||
import { useRef, useState } from "react";
|
import { useRef, useState } from "react";
|
||||||
import type { FeedbackFeature } from "../lib/featureFeedbackConstants";
|
import type { FeedbackFeature } from "../lib/featureFeedbackConstants";
|
||||||
import { FEEDBACK_FEATURES } from "../lib/featureFeedbackConstants";
|
import { FEEDBACK_FEATURES } from "../lib/featureFeedbackConstants";
|
||||||
import { invokeCmd } from "../lib/tauri";
|
import { rpc } from "../lib/rpc";
|
||||||
import { hideToastById, showToast } from "../lib/toast";
|
import { hideToastById, showToast } from "../lib/toast";
|
||||||
import { Button } from "./core/Button";
|
import { Button } from "./core/Button";
|
||||||
import { Input } from "./core/Input";
|
import { Input } from "./core/Input";
|
||||||
@@ -31,7 +31,7 @@ export function FeedbackToast({ feature, onDone }: Props) {
|
|||||||
onDone();
|
onDone();
|
||||||
|
|
||||||
// Fire-and-forget; failures are intentionally ignored
|
// Fire-and-forget; failures are intentionally ignored
|
||||||
invokeCmd("cmd_send_feedback", { feature, text: trimmedText }).catch(() => {});
|
rpc("cmd_send_feedback", { feature, text: trimmedText }).catch(() => {});
|
||||||
showToast({
|
showToast({
|
||||||
id: `feature-feedback-${feature}`,
|
id: `feature-feedback-${feature}`,
|
||||||
timeout: 3000,
|
timeout: 3000,
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { open } from "@tauri-apps/plugin-dialog";
|
|
||||||
import type { GrpcRequest } from "@yaakapp-internal/models";
|
import type { GrpcRequest } from "@yaakapp-internal/models";
|
||||||
import { Banner, HStack, Icon, InlineCode, VStack } from "@yaakapp-internal/ui";
|
import { Banner, HStack, Icon, InlineCode, VStack } from "@yaakapp-internal/ui";
|
||||||
import { useActiveRequest } from "../hooks/useActiveRequest";
|
import { useActiveRequest } from "../hooks/useActiveRequest";
|
||||||
@@ -8,6 +7,7 @@ import { pluralizeCount } from "../lib/pluralize";
|
|||||||
import { Button } from "./core/Button";
|
import { Button } from "./core/Button";
|
||||||
import { IconButton } from "./core/IconButton";
|
import { IconButton } from "./core/IconButton";
|
||||||
import { Link } from "./core/Link";
|
import { Link } from "./core/Link";
|
||||||
|
import { platform } from "@yaakapp-internal/platform";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onDone: () => void;
|
onDone: () => void;
|
||||||
@@ -45,7 +45,7 @@ function GrpcProtoSelectionDialogWithRequest({ request }: Props & { request: Grp
|
|||||||
color="primary"
|
color="primary"
|
||||||
variant="border"
|
variant="border"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
const selected = await open({
|
const selected = await platform.dialog.open({
|
||||||
title: "Select Proto Files",
|
title: "Select Proto Files",
|
||||||
multiple: true,
|
multiple: true,
|
||||||
filters: [{ name: "Proto Files", extensions: ["proto"] }],
|
filters: [{ name: "Proto Files", extensions: ["proto"] }],
|
||||||
@@ -63,7 +63,7 @@ function GrpcProtoSelectionDialogWithRequest({ request }: Props & { request: Grp
|
|||||||
variant="border"
|
variant="border"
|
||||||
color="primary"
|
color="primary"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
const selected = await open({
|
const selected = await platform.dialog.open({
|
||||||
title: "Select Proto Directory",
|
title: "Select Proto Directory",
|
||||||
directory: true,
|
directory: true,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ import classNames from "classnames";
|
|||||||
import { atom, useAtomValue } from "jotai";
|
import { atom, useAtomValue } from "jotai";
|
||||||
import type { CSSProperties } from "react";
|
import type { CSSProperties } from "react";
|
||||||
import { lazy, Suspense, useCallback, useMemo, useRef, useState } from "react";
|
import { lazy, Suspense, useCallback, useMemo, useRef, useState } from "react";
|
||||||
|
import { importCurl, looksLikeCurl } from "../commands/importCurl";
|
||||||
import { allRequestUrlsAtom } from "../hooks/useAllRequests";
|
import { allRequestUrlsAtom } from "../hooks/useAllRequests";
|
||||||
import { useAuthTab } from "../hooks/useAuthTab";
|
import { useAuthTab } from "../hooks/useAuthTab";
|
||||||
import { useCancelHttpResponse } from "../hooks/useCancelHttpResponse";
|
import { useCancelHttpResponse } from "../hooks/useCancelHttpResponse";
|
||||||
import { useHeadersTab } from "../hooks/useHeadersTab";
|
import { useHeadersTab } from "../hooks/useHeadersTab";
|
||||||
import { useImportCurl } from "../hooks/useImportCurl";
|
|
||||||
import { useInheritedHeaders } from "../hooks/useInheritedHeaders";
|
import { useInheritedHeaders } from "../hooks/useInheritedHeaders";
|
||||||
import { usePinnedHttpResponse } from "../hooks/usePinnedHttpResponse";
|
import { usePinnedHttpResponse } from "../hooks/usePinnedHttpResponse";
|
||||||
import { useRequestEditor, useRequestEditorEvent } from "../hooks/useRequestEditor";
|
import { useRequestEditor, useRequestEditorEvent } from "../hooks/useRequestEditor";
|
||||||
@@ -278,7 +278,6 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
|
|||||||
const { activeResponse } = usePinnedHttpResponse(activeRequestId);
|
const { activeResponse } = usePinnedHttpResponse(activeRequestId);
|
||||||
const { mutate: cancelResponse } = useCancelHttpResponse(activeResponse?.id ?? null);
|
const { mutate: cancelResponse } = useCancelHttpResponse(activeResponse?.id ?? null);
|
||||||
const updateKey = useRequestUpdateKey(activeRequestId);
|
const updateKey = useRequestUpdateKey(activeRequestId);
|
||||||
const { mutate: importCurl } = useImportCurl();
|
|
||||||
|
|
||||||
const handleBodyChange = useCallback(
|
const handleBodyChange = useCallback(
|
||||||
(body: HttpRequest["body"]) => patchModelDebounced(activeRequest, { body }),
|
(body: HttpRequest["body"]) => patchModelDebounced(activeRequest, { body }),
|
||||||
@@ -299,8 +298,8 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
|
|||||||
|
|
||||||
const handlePaste = useCallback(
|
const handlePaste = useCallback(
|
||||||
async (e: ClipboardEvent, text: string) => {
|
async (e: ClipboardEvent, text: string) => {
|
||||||
if (text.startsWith("curl ")) {
|
if (looksLikeCurl(text)) {
|
||||||
importCurl({ overwriteRequestId: activeRequestId, command: text });
|
importCurl.mutate({ overwriteRequestId: activeRequestId, command: text });
|
||||||
} else {
|
} else {
|
||||||
const patch = prepareImportQuerystring(text);
|
const patch = prepareImportQuerystring(text);
|
||||||
if (patch != null) {
|
if (patch != null) {
|
||||||
@@ -322,7 +321,7 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[activeRequest, activeRequestId, forceParamsRefresh, forceUrlRefresh, importCurl],
|
[activeRequest, activeRequestId, forceParamsRefresh, forceUrlRefresh],
|
||||||
);
|
);
|
||||||
const handleSend = useCallback(
|
const handleSend = useCallback(
|
||||||
() => sendRequest(activeRequest.id ?? null),
|
() => sendRequest(activeRequest.id ?? null),
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { useCopyHttpResponse } from "../hooks/useCopyHttpResponse";
|
|||||||
import { useHttpResponseEvents } from "../hooks/useHttpResponseEvents";
|
import { useHttpResponseEvents } from "../hooks/useHttpResponseEvents";
|
||||||
import { usePinnedHttpResponse } from "../hooks/usePinnedHttpResponse";
|
import { usePinnedHttpResponse } from "../hooks/usePinnedHttpResponse";
|
||||||
import { useResponseBodyBytes, useResponseBodyText } from "../hooks/useResponseBodyText";
|
import { useResponseBodyBytes, useResponseBodyText } from "../hooks/useResponseBodyText";
|
||||||
|
import { useResponseBodyUrl } from "../hooks/useResponseBodyUrl";
|
||||||
import { useResponseViewMode } from "../hooks/useResponseViewMode";
|
import { useResponseViewMode } from "../hooks/useResponseViewMode";
|
||||||
import { useSaveResponse } from "../hooks/useSaveResponse";
|
import { useSaveResponse } from "../hooks/useSaveResponse";
|
||||||
import { useTimelineViewMode } from "../hooks/useTimelineViewMode";
|
import { useTimelineViewMode } from "../hooks/useTimelineViewMode";
|
||||||
@@ -409,14 +410,13 @@ function EnsureCompleteResponse({
|
|||||||
Component,
|
Component,
|
||||||
}: {
|
}: {
|
||||||
response: HttpResponse;
|
response: HttpResponse;
|
||||||
Component: ComponentType<{ bodyPath: string }>;
|
Component: ComponentType<{ bodyUrl: string }>;
|
||||||
}) {
|
}) {
|
||||||
if (response.bodyPath === null) {
|
// Wait until the response has been fully-downloaded before asking for it
|
||||||
return <div>Empty response body</div>;
|
const complete = response.state === "closed";
|
||||||
}
|
const bodyUrl = useResponseBodyUrl(complete ? response : null);
|
||||||
|
|
||||||
// Wait until the response has been fully-downloaded
|
if (!complete || bodyUrl.isPending) {
|
||||||
if (response.state !== "closed") {
|
|
||||||
return (
|
return (
|
||||||
<EmptyStateText>
|
<EmptyStateText>
|
||||||
<LoadingIcon />
|
<LoadingIcon />
|
||||||
@@ -424,7 +424,15 @@ function EnsureCompleteResponse({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Component bodyPath={response.bodyPath} />;
|
if (bodyUrl.error) {
|
||||||
|
return <Banner color="danger">{String(bodyUrl.error)}</Banner>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bodyUrl.data == null) {
|
||||||
|
return <div>Empty response body</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Component bodyUrl={bodyUrl.data} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function HttpSvgViewer({ response }: { response: HttpResponse }) {
|
function HttpSvgViewer({ response }: { response: HttpResponse }) {
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
import { platform, useCapability } from "@yaakapp-internal/platform";
|
||||||
|
import { Icon } from "@yaakapp-internal/ui";
|
||||||
|
import * as m from "motion/react-m";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { importCurl, looksLikeCurl } from "../commands/importCurl";
|
||||||
|
import { useWindowFocus } from "../hooks/useWindowFocus";
|
||||||
|
import { showToast } from "../lib/toast";
|
||||||
|
import { Button } from "./core/Button";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Offers to create a request from a Curl command on the clipboard. A host that can read
|
||||||
|
* the clipboard on its own offers it whenever the window is focused; one that would have
|
||||||
|
* to prompt for permission first waits for the user to paste instead.
|
||||||
|
*/
|
||||||
|
export function ImportCurl() {
|
||||||
|
return useCapability("clipboardRead") ? <ImportCurlButton /> : <ImportCurlOnPaste />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ImportCurlButton() {
|
||||||
|
const focused = useWindowFocus();
|
||||||
|
const [clipboardText, setClipboardText] = useState("");
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
// oxlint-disable-next-line react-hooks/exhaustive-deps -- none
|
||||||
|
useEffect(() => {
|
||||||
|
void platform.clipboard.readText().then(setClipboardText);
|
||||||
|
}, [focused]);
|
||||||
|
|
||||||
|
if (!looksLikeCurl(clipboardText)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<m.div
|
||||||
|
initial={{ opacity: 0, scale: 0 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
transition={{ delay: 0.5 }}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
size="2xs"
|
||||||
|
variant="border"
|
||||||
|
color="success"
|
||||||
|
className="rounded-full"
|
||||||
|
rightSlot={<Icon icon="import" size="sm" />}
|
||||||
|
isLoading={isLoading}
|
||||||
|
title="Import Curl command from clipboard"
|
||||||
|
onClick={async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
await importCurl.mutateAsync({ command: clipboardText });
|
||||||
|
setClipboardText(""); // Hide the button until the clipboard changes
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Failed to import curl", e);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Import Curl
|
||||||
|
</Button>
|
||||||
|
</m.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ImportCurlOnPaste() {
|
||||||
|
useEffect(() => {
|
||||||
|
const handlePaste = (e: ClipboardEvent) => {
|
||||||
|
const command = e.clipboardData?.getData("text/plain") ?? "";
|
||||||
|
if (!looksLikeCurl(command)) return;
|
||||||
|
|
||||||
|
// Editable targets keep the text as text. The URL editor imports it itself.
|
||||||
|
if (isEditable(e.target)) return;
|
||||||
|
|
||||||
|
showToast({
|
||||||
|
id: "curl-paste",
|
||||||
|
color: "success",
|
||||||
|
message: (
|
||||||
|
<div>
|
||||||
|
<h2 className="font-semibold">Curl command detected</h2>
|
||||||
|
<p className="text-text-subtle text-sm">Create a request from the pasted command?</p>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
action: ({ hide }) => (
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
color="success"
|
||||||
|
className="mr-auto min-w-20"
|
||||||
|
rightSlot={<Icon icon="import" size="sm" />}
|
||||||
|
onClick={() => {
|
||||||
|
hide();
|
||||||
|
importCurl.mutate({ command });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Create Request
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("paste", handlePaste);
|
||||||
|
return () => document.removeEventListener("paste", handlePaste);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isEditable(target: EventTarget | null) {
|
||||||
|
if (!(target instanceof HTMLElement)) return false;
|
||||||
|
if (target.isContentEditable) return true;
|
||||||
|
if (target.closest(".cm-editor") != null) return true;
|
||||||
|
return ["INPUT", "TEXTAREA", "SELECT"].includes(target.tagName);
|
||||||
|
}
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
import { clear, readText } from "@tauri-apps/plugin-clipboard-manager";
|
|
||||||
import * as m from "motion/react-m";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useImportCurl } from "../hooks/useImportCurl";
|
|
||||||
import { useWindowFocus } from "../hooks/useWindowFocus";
|
|
||||||
import { Button } from "./core/Button";
|
|
||||||
import { Icon } from "@yaakapp-internal/ui";
|
|
||||||
|
|
||||||
export function ImportCurlButton() {
|
|
||||||
const focused = useWindowFocus();
|
|
||||||
const [clipboardText, setClipboardText] = useState("");
|
|
||||||
|
|
||||||
const importCurl = useImportCurl();
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
|
|
||||||
// oxlint-disable-next-line react-hooks/exhaustive-deps -- none
|
|
||||||
useEffect(() => {
|
|
||||||
void readText().then(setClipboardText);
|
|
||||||
}, [focused]);
|
|
||||||
|
|
||||||
if (!clipboardText?.trim().startsWith("curl ")) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<m.div
|
|
||||||
initial={{ opacity: 0, scale: 0 }}
|
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
|
||||||
transition={{ delay: 0.5 }}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
size="2xs"
|
|
||||||
variant="border"
|
|
||||||
color="success"
|
|
||||||
className="rounded-full"
|
|
||||||
rightSlot={<Icon icon="import" size="sm" />}
|
|
||||||
isLoading={isLoading}
|
|
||||||
title="Import Curl command from clipboard"
|
|
||||||
onClick={async () => {
|
|
||||||
setIsLoading(true);
|
|
||||||
try {
|
|
||||||
await importCurl.mutateAsync({ command: clipboardText });
|
|
||||||
await clear(); // Clear the clipboard so the button goes away
|
|
||||||
setClipboardText("");
|
|
||||||
} catch (e) {
|
|
||||||
console.log("Failed to import curl", e);
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Import Curl
|
|
||||||
</Button>
|
|
||||||
</m.div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,56 +1,138 @@
|
|||||||
import { VStack } from "@yaakapp-internal/ui";
|
import { platform } from "@yaakapp-internal/platform";
|
||||||
import { useState } from "react";
|
import { Icon, VStack } from "@yaakapp-internal/ui";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { useLocalStorage } from "react-use";
|
import { useLocalStorage } from "react-use";
|
||||||
import { CommercialUseBanner } from "./CommercialUseBanner";
|
import { CommercialUseBanner } from "./CommercialUseBanner";
|
||||||
import { Button } from "./core/Button";
|
import { Button } from "./core/Button";
|
||||||
import { SelectFile } from "./SelectFile";
|
import { PlainInput } from "./core/PlainInput";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
importData: (filePath: string) => Promise<void>;
|
importFile: (filePath: string) => Promise<void>;
|
||||||
|
importUrl: (url: string) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ImportDataDialog({ importData }: Props) {
|
/**
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
* An absolute or relative path is unambiguously a file. Everything else is treated as a URL, so a
|
||||||
const [filePath, setFilePath] = useLocalStorage<string | null>("importFilePath", null);
|
* bare host like `example.com/openapi.json` still works (the backend defaults it to https).
|
||||||
|
*/
|
||||||
|
function isFilePath(value: string): boolean {
|
||||||
return (
|
return (
|
||||||
<VStack space={5} className="pb-4">
|
value.startsWith("/") ||
|
||||||
<CommercialUseBanner source="data-import" title="Importing work data?" />
|
value.startsWith("./") ||
|
||||||
|
value.startsWith("../") ||
|
||||||
|
value.startsWith("~/") ||
|
||||||
|
value.startsWith("\\\\") ||
|
||||||
|
/^[a-zA-Z]:[\\/]/.test(value)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
<VStack space={1}>
|
function fileName(path: string): string {
|
||||||
<ul className="list-disc pl-5">
|
return path.split(/[/\\]/).at(-1) || path;
|
||||||
<li>OpenAPI 3.0, 3.1</li>
|
}
|
||||||
<li>Postman Collection v2, v2.1</li>
|
|
||||||
<li>Insomnia v4+</li>
|
export function ImportDataDialog({ importFile, importUrl }: Props) {
|
||||||
<li>Swagger 2.0</li>
|
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||||
<li>
|
// A file path or a URL. Both inputs write here, so there is only ever one thing to import
|
||||||
Curl commands <em className="text-text-subtle">(or paste into URL)</em>
|
const [source, setSource] = useLocalStorage<string | null>("importPathOrUrl", null);
|
||||||
</li>
|
const [forceUpdateKey, setForceUpdateKey] = useState<number>(0);
|
||||||
</ul>
|
const [isHovering, setIsHovering] = useState<boolean>(false);
|
||||||
</VStack>
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
<VStack space={2}>
|
const trimmedSource = source?.trim() ?? "";
|
||||||
<SelectFile
|
const filePath = isFilePath(trimmedSource) ? trimmedSource : null;
|
||||||
filePath={filePath ?? null}
|
|
||||||
onChange={({ filePath }) => setFilePath(filePath)}
|
const selectSource = (value: string) => {
|
||||||
/>
|
setSource(value);
|
||||||
{filePath && (
|
// Remount the input so it shows the path of the newly-picked file
|
||||||
<Button
|
setForceUpdateKey((k) => k + 1);
|
||||||
color="primary"
|
};
|
||||||
disabled={!filePath || isLoading}
|
|
||||||
isLoading={isLoading}
|
// Accept a file dropped anywhere on the dialog, the way SelectFile does for its button
|
||||||
size="sm"
|
useEffect(() => {
|
||||||
onClick={async () => {
|
return platform.window.onDragDrop((event) => {
|
||||||
|
if (event.type === "over") {
|
||||||
|
const p = event.position;
|
||||||
|
const r = ref.current?.getBoundingClientRect();
|
||||||
|
if (r == null) return;
|
||||||
|
setIsHovering(p.x >= r.left && p.x <= r.right && p.y >= r.top && p.y <= r.bottom);
|
||||||
|
} else if (event.type === "drop" && isHovering) {
|
||||||
|
const p = event.paths[0];
|
||||||
|
if (p) selectSource(p);
|
||||||
|
setIsHovering(false);
|
||||||
|
} else {
|
||||||
|
setIsHovering(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [isHovering, setSource]);
|
||||||
|
|
||||||
|
const handleSelectFile = async () => {
|
||||||
|
const selected = await platform.dialog.open({ title: "Select File", multiple: false });
|
||||||
|
if (selected == null) return;
|
||||||
|
selectSource(selected);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleImport = async () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
await importData(filePath);
|
if (filePath != null) {
|
||||||
|
await importFile(filePath);
|
||||||
|
} else {
|
||||||
|
await importUrl(trimmedSource);
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
}}
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VStack ref={ref} space={4} className="pb-4">
|
||||||
|
<CommercialUseBanner source="data-import" title="Importing work data?" />
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleSelectFile}
|
||||||
|
className={classNames(
|
||||||
|
"w-full rounded-lg border border-dashed px-4 py-6",
|
||||||
|
"flex flex-col items-center gap-1 text-center",
|
||||||
|
isHovering ? "border-notice bg-surface-highlight" : "border-border hover:border-text",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon icon="folder_input" className="text-text-subtlest w-8! h-8! mb-2" />
|
||||||
|
{/* Fixed height so the region doesn't resize between the empty and selected states */}
|
||||||
|
<div className="h-6 w-full flex items-center justify-center">
|
||||||
|
{filePath == null ? (
|
||||||
|
<div className="text-text">
|
||||||
|
<strong className="font-semibold">Choose a file</strong> or drag it here
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-text font-mono text-xs max-w-full truncate" title={filePath}>
|
||||||
|
{fileName(filePath)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-text-subtlest">
|
||||||
|
Supports OpenAPI, Swagger, Postman, Insomnia, and curl
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<VStack space={2}>
|
||||||
|
<PlainInput
|
||||||
|
label="Or enter a file path or URL"
|
||||||
|
size="sm"
|
||||||
|
placeholder="https://example.com/openapi.json"
|
||||||
|
defaultValue={source ?? ""}
|
||||||
|
forceUpdateKey={String(forceUpdateKey)}
|
||||||
|
onChange={setSource}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
color="primary"
|
||||||
|
disabled={trimmedSource === "" || isLoading}
|
||||||
|
isLoading={isLoading}
|
||||||
|
size="sm"
|
||||||
|
onClick={handleImport}
|
||||||
>
|
>
|
||||||
{isLoading ? "Importing" : "Import"}
|
{isLoading ? "Importing" : "Import"}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
|
||||||
</VStack>
|
</VStack>
|
||||||
</VStack>
|
</VStack>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { openUrl } from "@tauri-apps/plugin-opener";
|
|
||||||
import type { LicenseCheckStatus } from "@yaakapp-internal/license";
|
import type { LicenseCheckStatus } from "@yaakapp-internal/license";
|
||||||
import { useLicense } from "@yaakapp-internal/license";
|
import { useLicense } from "@yaakapp-internal/license";
|
||||||
import { settingsAtom } from "@yaakapp-internal/models";
|
import { settingsAtom } from "@yaakapp-internal/models";
|
||||||
@@ -14,6 +13,7 @@ import type { ButtonProps } from "./core/Button";
|
|||||||
import { Dropdown, type DropdownItem } from "./core/Dropdown";
|
import { Dropdown, type DropdownItem } from "./core/Dropdown";
|
||||||
import { Icon } from "@yaakapp-internal/ui";
|
import { Icon } from "@yaakapp-internal/ui";
|
||||||
import { PillButton } from "./core/PillButton";
|
import { PillButton } from "./core/PillButton";
|
||||||
|
import { platform } from "@yaakapp-internal/platform";
|
||||||
|
|
||||||
const dismissedAtom = atomWithKVStorage<string | null>("dismissed_license_expired", null);
|
const dismissedAtom = atomWithKVStorage<string | null>("dismissed_license_expired", null);
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ function getDetail(
|
|||||||
leftSlot: <Icon icon="gift" />,
|
leftSlot: <Icon icon="gift" />,
|
||||||
rightSlot: <Icon icon="external_link" size="sm" className="opacity-disabled" />,
|
rightSlot: <Icon icon="external_link" size="sm" className="opacity-disabled" />,
|
||||||
hidden: data.data.changes === 0 || data.data.changesUrl == null,
|
hidden: data.data.changes === 0 || data.data.changesUrl == null,
|
||||||
onSelect: () => openUrl(data.data.changesUrl ?? ""),
|
onSelect: () => platform.openUrl(data.data.changesUrl ?? ""),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "separator",
|
type: "separator",
|
||||||
@@ -63,7 +63,7 @@ function getDetail(
|
|||||||
leftSlot: <Icon icon="refresh" />,
|
leftSlot: <Icon icon="refresh" />,
|
||||||
rightSlot: <Icon icon="external_link" size="sm" className="opacity-disabled" />,
|
rightSlot: <Icon icon="external_link" size="sm" className="opacity-disabled" />,
|
||||||
hidden: data.data.changesUrl == null,
|
hidden: data.data.changesUrl == null,
|
||||||
onSelect: () => openUrl(data.data.billingUrl),
|
onSelect: () => platform.openUrl(data.data.billingUrl),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Enter License Key",
|
label: "Enter License Key",
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { convertFileSrc } from "@tauri-apps/api/core";
|
|
||||||
import { resolveResource } from "@tauri-apps/api/path";
|
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
import { platform } from "@yaakapp-internal/platform";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
src: string;
|
src: string;
|
||||||
@@ -12,8 +11,8 @@ export function LocalImage({ src: srcPath, className }: Props) {
|
|||||||
const src = useQuery({
|
const src = useQuery({
|
||||||
queryKey: ["local-image", srcPath],
|
queryKey: ["local-image", srcPath],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const p = await resolveResource(srcPath);
|
const p = await platform.files.resolveResource(srcPath);
|
||||||
return convertFileSrc(p);
|
return platform.files.url(p);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ import type {
|
|||||||
Folder,
|
Folder,
|
||||||
GrpcRequest,
|
GrpcRequest,
|
||||||
HttpRequest,
|
HttpRequest,
|
||||||
|
HttpVersion,
|
||||||
InheritedBoolSetting,
|
InheritedBoolSetting,
|
||||||
|
InheritedHttpVersionSetting,
|
||||||
InheritedIntSetting,
|
InheritedIntSetting,
|
||||||
WebsocketRequest,
|
WebsocketRequest,
|
||||||
Workspace,
|
Workspace,
|
||||||
@@ -13,6 +15,7 @@ import {
|
|||||||
modelSupportsSetting,
|
modelSupportsSetting,
|
||||||
type RequestSettingDefinition,
|
type RequestSettingDefinition,
|
||||||
SETTING_FOLLOW_REDIRECTS,
|
SETTING_FOLLOW_REDIRECTS,
|
||||||
|
SETTING_HTTP_VERSION,
|
||||||
SETTING_REQUEST_MESSAGE_SIZE,
|
SETTING_REQUEST_MESSAGE_SIZE,
|
||||||
SETTING_REQUEST_TIMEOUT,
|
SETTING_REQUEST_TIMEOUT,
|
||||||
SETTING_SEND_COOKIES,
|
SETTING_SEND_COOKIES,
|
||||||
@@ -21,6 +24,7 @@ import {
|
|||||||
} from "../lib/requestSettings";
|
} from "../lib/requestSettings";
|
||||||
import { Checkbox } from "./core/Checkbox";
|
import { Checkbox } from "./core/Checkbox";
|
||||||
import { PlainInput } from "./core/PlainInput";
|
import { PlainInput } from "./core/PlainInput";
|
||||||
|
import { Select } from "./core/Select";
|
||||||
import {
|
import {
|
||||||
SettingOverrideRow,
|
SettingOverrideRow,
|
||||||
SettingRow,
|
SettingRow,
|
||||||
@@ -38,37 +42,21 @@ interface Props {
|
|||||||
model: ModelWithSettings;
|
model: ModelWithSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
type ModelWithSettings =
|
type ModelWithSettings = Workspace | Folder | HttpRequest | WebsocketRequest | GrpcRequest;
|
||||||
| Workspace
|
|
||||||
| Folder
|
|
||||||
| HttpRequest
|
|
||||||
| WebsocketRequest
|
|
||||||
| GrpcRequest;
|
|
||||||
type ModelWithHttpSettings = Workspace | Folder | HttpRequest;
|
type ModelWithHttpSettings = Workspace | Folder | HttpRequest;
|
||||||
type ModelWithTlsSettings =
|
type ModelWithTlsSettings = Workspace | Folder | HttpRequest | WebsocketRequest | GrpcRequest;
|
||||||
| Workspace
|
type ModelWithCookieSettings = Workspace | Folder | HttpRequest | WebsocketRequest;
|
||||||
| Folder
|
type ModelWithMessageSizeSettings = Workspace | Folder | WebsocketRequest | GrpcRequest;
|
||||||
| HttpRequest
|
|
||||||
| WebsocketRequest
|
|
||||||
| GrpcRequest;
|
|
||||||
type ModelWithCookieSettings =
|
|
||||||
| Workspace
|
|
||||||
| Folder
|
|
||||||
| HttpRequest
|
|
||||||
| WebsocketRequest;
|
|
||||||
type ModelWithMessageSizeSettings =
|
|
||||||
| Workspace
|
|
||||||
| Folder
|
|
||||||
| WebsocketRequest
|
|
||||||
| GrpcRequest;
|
|
||||||
type BooleanSetting = boolean | InheritedBoolSetting;
|
type BooleanSetting = boolean | InheritedBoolSetting;
|
||||||
type IntegerSetting = number | InheritedIntSetting;
|
type IntegerSetting = number | InheritedIntSetting;
|
||||||
|
type HttpVersionSetting = HttpVersion | InheritedHttpVersionSetting;
|
||||||
type CookieSettingsPatch = {
|
type CookieSettingsPatch = {
|
||||||
settingSendCookies?: ModelWithCookieSettings["settingSendCookies"];
|
settingSendCookies?: ModelWithCookieSettings["settingSendCookies"];
|
||||||
settingStoreCookies?: ModelWithCookieSettings["settingStoreCookies"];
|
settingStoreCookies?: ModelWithCookieSettings["settingStoreCookies"];
|
||||||
};
|
};
|
||||||
type HttpSettingsPatch = {
|
type HttpSettingsPatch = {
|
||||||
settingFollowRedirects?: ModelWithHttpSettings["settingFollowRedirects"];
|
settingFollowRedirects?: ModelWithHttpSettings["settingFollowRedirects"];
|
||||||
|
settingHttpVersion?: ModelWithHttpSettings["settingHttpVersion"];
|
||||||
settingRequestTimeout?: ModelWithHttpSettings["settingRequestTimeout"];
|
settingRequestTimeout?: ModelWithHttpSettings["settingRequestTimeout"];
|
||||||
};
|
};
|
||||||
type TlsSettingsPatch = {
|
type TlsSettingsPatch = {
|
||||||
@@ -78,10 +66,7 @@ type MessageSizeSettingsPatch = {
|
|||||||
settingRequestMessageSize?: ModelWithMessageSizeSettings["settingRequestMessageSize"];
|
settingRequestMessageSize?: ModelWithMessageSizeSettings["settingRequestMessageSize"];
|
||||||
};
|
};
|
||||||
|
|
||||||
export function ModelSettingsEditor({
|
export function ModelSettingsEditor({ model, showSectionTitles = false }: Props) {
|
||||||
model,
|
|
||||||
showSectionTitles = false,
|
|
||||||
}: Props) {
|
|
||||||
const ancestors = useModelAncestors(model);
|
const ancestors = useModelAncestors(model);
|
||||||
const supportsHttpSettings = modelSupportsHttpSettings(model);
|
const supportsHttpSettings = modelSupportsHttpSettings(model);
|
||||||
const supportsCookieSettings = modelSupportsCookieSettings(model);
|
const supportsCookieSettings = modelSupportsCookieSettings(model);
|
||||||
@@ -154,12 +139,26 @@ export function ModelSettingsEditor({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{supportsHttpSettings && (
|
||||||
|
<HttpVersionSettingRow
|
||||||
|
settingDefinition={SETTING_HTTP_VERSION}
|
||||||
|
setting={model.settingHttpVersion}
|
||||||
|
inheritedValue={resolveInheritedValue(
|
||||||
|
ancestors,
|
||||||
|
SETTING_HTTP_VERSION.modelKey,
|
||||||
|
model.settingHttpVersion,
|
||||||
|
)}
|
||||||
|
onChange={(settingHttpVersion) =>
|
||||||
|
patchHttpSettings(model, {
|
||||||
|
settingHttpVersion,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
)}
|
)}
|
||||||
{supportsCookieSettings && (
|
{supportsCookieSettings && (
|
||||||
<SettingsSection
|
<SettingsSection title={supportsTlsSettings || showSectionTitles ? "Cookies" : null}>
|
||||||
title={supportsTlsSettings || showSectionTitles ? "Cookies" : null}
|
|
||||||
>
|
|
||||||
<BooleanSettingRow
|
<BooleanSettingRow
|
||||||
settingDefinition={SETTING_SEND_COOKIES}
|
settingDefinition={SETTING_SEND_COOKIES}
|
||||||
setting={model.settingSendCookies}
|
setting={model.settingSendCookies}
|
||||||
@@ -195,7 +194,7 @@ export function ModelSettingsEditor({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function countOverriddenSettings(model: ModelWithSettings) {
|
export function countOverriddenSettings(model: ModelWithSettings) {
|
||||||
const settings: (BooleanSetting | IntegerSetting)[] = [];
|
const settings: (BooleanSetting | IntegerSetting | HttpVersionSetting)[] = [];
|
||||||
|
|
||||||
if (modelSupportsCookieSettings(model)) {
|
if (modelSupportsCookieSettings(model)) {
|
||||||
settings.push(model.settingSendCookies, model.settingStoreCookies);
|
settings.push(model.settingSendCookies, model.settingStoreCookies);
|
||||||
@@ -204,22 +203,22 @@ export function countOverriddenSettings(model: ModelWithSettings) {
|
|||||||
settings.push(model.settingValidateCertificates);
|
settings.push(model.settingValidateCertificates);
|
||||||
|
|
||||||
if (modelSupportsHttpSettings(model)) {
|
if (modelSupportsHttpSettings(model)) {
|
||||||
settings.push(model.settingFollowRedirects, model.settingRequestTimeout);
|
settings.push(
|
||||||
|
model.settingFollowRedirects,
|
||||||
|
model.settingRequestTimeout,
|
||||||
|
model.settingHttpVersion,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (modelSupportsMessageSizeSettings(model)) {
|
if (modelSupportsMessageSizeSettings(model)) {
|
||||||
settings.push(model.settingRequestMessageSize);
|
settings.push(model.settingRequestMessageSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
return settings.filter(
|
return settings.filter((setting) => isInheritedSetting(setting) && setting.enabled === true)
|
||||||
(setting) => isInheritedSetting(setting) && setting.enabled === true,
|
.length;
|
||||||
).length;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function patchCookieSettings(
|
function patchCookieSettings(model: ModelWithCookieSettings, patch: Partial<CookieSettingsPatch>) {
|
||||||
model: ModelWithCookieSettings,
|
|
||||||
patch: Partial<CookieSettingsPatch>,
|
|
||||||
) {
|
|
||||||
switch (model.model) {
|
switch (model.model) {
|
||||||
case "workspace":
|
case "workspace":
|
||||||
return patchModel(model, patch as Partial<Workspace>);
|
return patchModel(model, patch as Partial<Workspace>);
|
||||||
@@ -232,10 +231,7 @@ function patchCookieSettings(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function patchHttpSettings(
|
function patchHttpSettings(model: ModelWithHttpSettings, patch: Partial<HttpSettingsPatch>) {
|
||||||
model: ModelWithHttpSettings,
|
|
||||||
patch: Partial<HttpSettingsPatch>,
|
|
||||||
) {
|
|
||||||
switch (model.model) {
|
switch (model.model) {
|
||||||
case "workspace":
|
case "workspace":
|
||||||
return patchModel(model, patch as Partial<Workspace>);
|
return patchModel(model, patch as Partial<Workspace>);
|
||||||
@@ -246,10 +242,7 @@ function patchHttpSettings(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function patchTlsSettings(
|
function patchTlsSettings(model: ModelWithTlsSettings, patch: Partial<TlsSettingsPatch>) {
|
||||||
model: ModelWithTlsSettings,
|
|
||||||
patch: Partial<TlsSettingsPatch>,
|
|
||||||
) {
|
|
||||||
switch (model.model) {
|
switch (model.model) {
|
||||||
case "workspace":
|
case "workspace":
|
||||||
return patchModel(model, patch as Partial<Workspace>);
|
return patchModel(model, patch as Partial<Workspace>);
|
||||||
@@ -280,21 +273,15 @@ function patchMessageSizeSettings(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function modelSupportsHttpSettings(
|
function modelSupportsHttpSettings(model: ModelWithSettings): model is ModelWithHttpSettings {
|
||||||
model: ModelWithSettings,
|
|
||||||
): model is ModelWithHttpSettings {
|
|
||||||
return modelSupportsSetting(model, SETTING_REQUEST_TIMEOUT);
|
return modelSupportsSetting(model, SETTING_REQUEST_TIMEOUT);
|
||||||
}
|
}
|
||||||
|
|
||||||
function modelSupportsCookieSettings(
|
function modelSupportsCookieSettings(model: ModelWithSettings): model is ModelWithCookieSettings {
|
||||||
model: ModelWithSettings,
|
|
||||||
): model is ModelWithCookieSettings {
|
|
||||||
return modelSupportsSetting(model, SETTING_SEND_COOKIES);
|
return modelSupportsSetting(model, SETTING_SEND_COOKIES);
|
||||||
}
|
}
|
||||||
|
|
||||||
function modelSupportsTlsSettings(
|
function modelSupportsTlsSettings(model: ModelWithSettings): model is ModelWithTlsSettings {
|
||||||
model: ModelWithSettings,
|
|
||||||
): model is ModelWithTlsSettings {
|
|
||||||
return modelSupportsSetting(model, SETTING_VALIDATE_CERTIFICATES);
|
return modelSupportsSetting(model, SETTING_VALIDATE_CERTIFICATES);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -317,11 +304,7 @@ function BooleanSettingRow({
|
|||||||
}) {
|
}) {
|
||||||
const inherited = isInheritedSetting(setting);
|
const inherited = isInheritedSetting(setting);
|
||||||
const overridden = inherited ? setting.enabled === true : false;
|
const overridden = inherited ? setting.enabled === true : false;
|
||||||
const value = inherited
|
const value = inherited ? (overridden ? setting.value : inheritedValue) : setting;
|
||||||
? overridden
|
|
||||||
? setting.value
|
|
||||||
: inheritedValue
|
|
||||||
: setting;
|
|
||||||
|
|
||||||
if (!inherited) {
|
if (!inherited) {
|
||||||
return (
|
return (
|
||||||
@@ -352,6 +335,63 @@ function BooleanSettingRow({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const HTTP_VERSION_OPTIONS: { label: string; value: HttpVersion }[] = [
|
||||||
|
{ label: "Automatic", value: "auto" },
|
||||||
|
{ label: "HTTP/1.1", value: "http1" },
|
||||||
|
{ label: "HTTP/2", value: "http2" },
|
||||||
|
];
|
||||||
|
|
||||||
|
function HttpVersionSettingRow({
|
||||||
|
inheritedValue,
|
||||||
|
setting,
|
||||||
|
settingDefinition,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
inheritedValue: HttpVersion;
|
||||||
|
setting: HttpVersionSetting;
|
||||||
|
settingDefinition: RequestSettingDefinition<"settingHttpVersion">;
|
||||||
|
onChange: (setting: HttpVersionSetting) => void;
|
||||||
|
}) {
|
||||||
|
const inherited = isInheritedSetting(setting);
|
||||||
|
const overridden = inherited ? setting.enabled === true : false;
|
||||||
|
const value = inherited ? (overridden ? setting.value : inheritedValue) : setting;
|
||||||
|
|
||||||
|
if (!inherited) {
|
||||||
|
return (
|
||||||
|
<SettingRow title={settingDefinition.title} description={settingDefinition.description}>
|
||||||
|
<Select
|
||||||
|
hideLabel
|
||||||
|
name={settingDefinition.modelKey}
|
||||||
|
label={settingDefinition.title}
|
||||||
|
size="sm"
|
||||||
|
value={value}
|
||||||
|
options={HTTP_VERSION_OPTIONS}
|
||||||
|
onChange={(value) => onChange(value)}
|
||||||
|
/>
|
||||||
|
</SettingRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingOverrideRow
|
||||||
|
title={settingDefinition.title}
|
||||||
|
description={settingDefinition.description}
|
||||||
|
overridden={overridden}
|
||||||
|
onResetOverride={() => onChange({ ...setting, enabled: false })}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
hideLabel
|
||||||
|
name={settingDefinition.modelKey}
|
||||||
|
label={settingDefinition.title}
|
||||||
|
size="sm"
|
||||||
|
value={value}
|
||||||
|
options={HTTP_VERSION_OPTIONS}
|
||||||
|
onChange={(value) => onChange({ ...setting, enabled: true, value })}
|
||||||
|
/>
|
||||||
|
</SettingOverrideRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function IntegerSettingRow({
|
function IntegerSettingRow({
|
||||||
inheritedValue,
|
inheritedValue,
|
||||||
setting,
|
setting,
|
||||||
@@ -365,18 +405,11 @@ function IntegerSettingRow({
|
|||||||
}) {
|
}) {
|
||||||
const inherited = isInheritedSetting(setting);
|
const inherited = isInheritedSetting(setting);
|
||||||
const overridden = inherited ? setting.enabled === true : false;
|
const overridden = inherited ? setting.enabled === true : false;
|
||||||
const value = inherited
|
const value = inherited ? (overridden ? setting.value : inheritedValue) : setting;
|
||||||
? overridden
|
|
||||||
? setting.value
|
|
||||||
: inheritedValue
|
|
||||||
: setting;
|
|
||||||
|
|
||||||
if (!inherited) {
|
if (!inherited) {
|
||||||
return (
|
return (
|
||||||
<SettingRow
|
<SettingRow title={settingDefinition.title} description={settingDefinition.description}>
|
||||||
title={settingDefinition.title}
|
|
||||||
description={settingDefinition.description}
|
|
||||||
>
|
|
||||||
<NumberUnitInput
|
<NumberUnitInput
|
||||||
name={settingDefinition.modelKey}
|
name={settingDefinition.modelKey}
|
||||||
label={settingDefinition.title}
|
label={settingDefinition.title}
|
||||||
@@ -429,20 +462,13 @@ function MessageSizeSettingRow({
|
|||||||
}) {
|
}) {
|
||||||
const inherited = isInheritedSetting(setting);
|
const inherited = isInheritedSetting(setting);
|
||||||
const overridden = inherited ? setting.enabled === true : false;
|
const overridden = inherited ? setting.enabled === true : false;
|
||||||
const value = inherited
|
const value = inherited ? (overridden ? setting.value : inheritedValue) : setting;
|
||||||
? overridden
|
|
||||||
? setting.value
|
|
||||||
: inheritedValue
|
|
||||||
: setting;
|
|
||||||
const displayValue = formatMegabytes(value);
|
const displayValue = formatMegabytes(value);
|
||||||
const placeholder = "0";
|
const placeholder = "0";
|
||||||
|
|
||||||
if (!inherited) {
|
if (!inherited) {
|
||||||
return (
|
return (
|
||||||
<SettingRow
|
<SettingRow title={settingDefinition.title} description={settingDefinition.description}>
|
||||||
title={settingDefinition.title}
|
|
||||||
description={settingDefinition.description}
|
|
||||||
>
|
|
||||||
<MessageSizeInput
|
<MessageSizeInput
|
||||||
name={settingDefinition.modelKey}
|
name={settingDefinition.modelKey}
|
||||||
label={settingDefinition.title}
|
label={settingDefinition.title}
|
||||||
@@ -567,13 +593,18 @@ function resolveInheritedValue(
|
|||||||
key: BooleanWorkspaceSettingKey,
|
key: BooleanWorkspaceSettingKey,
|
||||||
fallback: BooleanSetting,
|
fallback: BooleanSetting,
|
||||||
): boolean;
|
): boolean;
|
||||||
|
function resolveInheritedValue(
|
||||||
|
ancestors: (Folder | Workspace)[],
|
||||||
|
key: "settingHttpVersion",
|
||||||
|
fallback: HttpVersionSetting,
|
||||||
|
): HttpVersion;
|
||||||
function resolveInheritedValue(
|
function resolveInheritedValue(
|
||||||
ancestors: (Folder | Workspace)[],
|
ancestors: (Folder | Workspace)[],
|
||||||
key: keyof WorkspaceSettings,
|
key: keyof WorkspaceSettings,
|
||||||
fallback: BooleanSetting | IntegerSetting,
|
fallback: BooleanSetting | IntegerSetting | HttpVersionSetting,
|
||||||
) {
|
) {
|
||||||
for (const ancestor of ancestors) {
|
for (const ancestor of ancestors) {
|
||||||
const setting = ancestor[key] as BooleanSetting | IntegerSetting;
|
const setting = ancestor[key] as BooleanSetting | IntegerSetting | HttpVersionSetting;
|
||||||
if (isInheritedSetting(setting)) {
|
if (isInheritedSetting(setting)) {
|
||||||
if (setting.enabled === true) {
|
if (setting.enabled === true) {
|
||||||
return setting.value;
|
return setting.value;
|
||||||
@@ -589,6 +620,7 @@ function resolveInheritedValue(
|
|||||||
type WorkspaceSettings = Pick<
|
type WorkspaceSettings = Pick<
|
||||||
Workspace,
|
Workspace,
|
||||||
| "settingFollowRedirects"
|
| "settingFollowRedirects"
|
||||||
|
| "settingHttpVersion"
|
||||||
| "settingRequestMessageSize"
|
| "settingRequestMessageSize"
|
||||||
| "settingRequestTimeout"
|
| "settingRequestTimeout"
|
||||||
| "settingSendCookies"
|
| "settingSendCookies"
|
||||||
@@ -598,14 +630,12 @@ type WorkspaceSettings = Pick<
|
|||||||
|
|
||||||
type BooleanWorkspaceSettingKey = Exclude<
|
type BooleanWorkspaceSettingKey = Exclude<
|
||||||
keyof WorkspaceSettings,
|
keyof WorkspaceSettings,
|
||||||
"settingRequestTimeout" | "settingRequestMessageSize"
|
"settingRequestTimeout" | "settingRequestMessageSize" | "settingHttpVersion"
|
||||||
>;
|
>;
|
||||||
|
|
||||||
function formatMegabytes(bytes: number) {
|
function formatMegabytes(bytes: number) {
|
||||||
const megabytes = bytes / BYTES_PER_MB;
|
const megabytes = bytes / BYTES_PER_MB;
|
||||||
return Number.isInteger(megabytes)
|
return Number.isInteger(megabytes) ? `${megabytes}` : megabytes.toFixed(3).replace(/\.?0+$/, "");
|
||||||
? `${megabytes}`
|
|
||||||
: megabytes.toFixed(3).replace(/\.?0+$/, "");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseMegabytes(value: string) {
|
function parseMegabytes(value: string) {
|
||||||
@@ -626,9 +656,5 @@ function isValidInteger(value: string) {
|
|||||||
function isValidMegabytes(value: string) {
|
function isValidMegabytes(value: string) {
|
||||||
if (value === "") return true;
|
if (value === "") return true;
|
||||||
const megabytes = Number(value);
|
const megabytes = Number(value);
|
||||||
return (
|
return Number.isFinite(megabytes) && megabytes >= 0 && megabytes <= MAX_MESSAGE_SIZE_MB;
|
||||||
Number.isFinite(megabytes) &&
|
|
||||||
megabytes >= 0 &&
|
|
||||||
megabytes <= MAX_MESSAGE_SIZE_MB
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { openUrl } from "@tauri-apps/plugin-opener";
|
|
||||||
import type { HttpResponse } from "@yaakapp-internal/models";
|
import type { HttpResponse } from "@yaakapp-internal/models";
|
||||||
import { format, formatDistanceToNowStrict } from "date-fns";
|
import { format, formatDistanceToNowStrict } from "date-fns";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
@@ -6,6 +5,7 @@ import { CountBadge } from "./core/CountBadge";
|
|||||||
import { DetailsBanner } from "./core/DetailsBanner";
|
import { DetailsBanner } from "./core/DetailsBanner";
|
||||||
import { IconButton } from "./core/IconButton";
|
import { IconButton } from "./core/IconButton";
|
||||||
import { KeyValueRow, KeyValueRows } from "./core/KeyValueRow";
|
import { KeyValueRow, KeyValueRows } from "./core/KeyValueRow";
|
||||||
|
import { platform } from "@yaakapp-internal/platform";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
response: HttpResponse;
|
response: HttpResponse;
|
||||||
@@ -45,7 +45,7 @@ export function ResponseHeaders({ response }: Props) {
|
|||||||
iconSize="sm"
|
iconSize="sm"
|
||||||
className="inline-block w-auto h-auto! opacity-50 hover:opacity-100"
|
className="inline-block w-auto h-auto! opacity-50 hover:opacity-100"
|
||||||
icon="external_link"
|
icon="external_link"
|
||||||
onClick={() => openUrl(response.url)}
|
onClick={() => platform.openUrl(response.url)}
|
||||||
title="Open in browser"
|
title="Open in browser"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { openUrl } from "@tauri-apps/plugin-opener";
|
|
||||||
import type { HttpResponse } from "@yaakapp-internal/models";
|
import type { HttpResponse } from "@yaakapp-internal/models";
|
||||||
import { IconButton } from "./core/IconButton";
|
import { IconButton } from "./core/IconButton";
|
||||||
import { KeyValueRow, KeyValueRows } from "./core/KeyValueRow";
|
import { KeyValueRow, KeyValueRows } from "./core/KeyValueRow";
|
||||||
|
import { platform } from "@yaakapp-internal/platform";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
response: HttpResponse;
|
response: HttpResponse;
|
||||||
@@ -26,7 +26,7 @@ export function ResponseInfo({ response }: Props) {
|
|||||||
iconSize="sm"
|
iconSize="sm"
|
||||||
className="inline-block w-auto ml-1 h-auto! opacity-50 hover:opacity-100"
|
className="inline-block w-auto ml-1 h-auto! opacity-50 hover:opacity-100"
|
||||||
icon="external_link"
|
icon="external_link"
|
||||||
onClick={() => openUrl(response.url)}
|
onClick={() => platform.openUrl(response.url)}
|
||||||
title="Open in browser"
|
title="Open in browser"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
|
import { platform } from "@yaakapp-internal/platform";
|
||||||
import { open } from "@tauri-apps/plugin-dialog";
|
|
||||||
import { HStack } from "@yaakapp-internal/ui";
|
import { HStack } from "@yaakapp-internal/ui";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import mime from "mime";
|
import mime from "mime";
|
||||||
@@ -41,7 +40,7 @@ export function SelectFile({
|
|||||||
...props
|
...props
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const handleClick = async () => {
|
const handleClick = async () => {
|
||||||
const filePath = await open({
|
const filePath = await platform.dialog.open({
|
||||||
title: directory ? "Select Folder" : "Select File",
|
title: directory ? "Select Folder" : "Select File",
|
||||||
multiple: false,
|
multiple: false,
|
||||||
directory,
|
directory,
|
||||||
@@ -64,32 +63,21 @@ export function SelectFile({
|
|||||||
// NOTE: This doesn't work for Windows since native drag-n-drop can't work at the same tmie
|
// NOTE: This doesn't work for Windows since native drag-n-drop can't work at the same tmie
|
||||||
// as browser drag-n-drop.
|
// as browser drag-n-drop.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let unlisten: (() => void) | undefined;
|
return platform.window.onDragDrop((event) => {
|
||||||
const setup = async () => {
|
if (event.type === "over") {
|
||||||
const webview = getCurrentWebviewWindow();
|
const p = event.position;
|
||||||
unlisten = await webview.onDragDropEvent((event) => {
|
|
||||||
if (event.payload.type === "over") {
|
|
||||||
const p = event.payload.position;
|
|
||||||
const r = ref.current?.getBoundingClientRect();
|
const r = ref.current?.getBoundingClientRect();
|
||||||
if (r == null) return;
|
if (r == null) return;
|
||||||
const isOver = p.x >= r.left && p.x <= r.right && p.y >= r.top && p.y <= r.bottom;
|
const isOver = p.x >= r.left && p.x <= r.right && p.y >= r.top && p.y <= r.bottom;
|
||||||
console.log("IS OVER", isOver);
|
|
||||||
setIsHovering(isOver);
|
setIsHovering(isOver);
|
||||||
} else if (event.payload.type === "drop" && isHovering) {
|
} else if (event.type === "drop" && isHovering) {
|
||||||
console.log("User dropped", event.payload.paths);
|
const p = event.paths[0];
|
||||||
const p = event.payload.paths[0];
|
|
||||||
if (p) onChange({ filePath: p, contentType: null });
|
if (p) onChange({ filePath: p, contentType: null });
|
||||||
setIsHovering(false);
|
setIsHovering(false);
|
||||||
} else {
|
} else {
|
||||||
console.log("File drop cancelled");
|
|
||||||
setIsHovering(false);
|
setIsHovering(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
|
||||||
setup().catch(console.error);
|
|
||||||
return () => {
|
|
||||||
if (unlisten) unlisten();
|
|
||||||
};
|
|
||||||
}, [isHovering, onChange]);
|
}, [isHovering, onChange]);
|
||||||
|
|
||||||
const filePathWithNameOverride = nameOverride ? `${filePath} (${nameOverride})` : filePath;
|
const filePathWithNameOverride = nameOverride ? `${filePath} (${nameOverride})` : filePath;
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import { useSearch } from "@tanstack/react-router";
|
import { platform } from "@yaakapp-internal/platform";
|
||||||
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
|
|
||||||
import { type } from "@tauri-apps/plugin-os";
|
|
||||||
import { useLicense } from "@yaakapp-internal/license";
|
import { useLicense } from "@yaakapp-internal/license";
|
||||||
import { pluginsAtom, settingsAtom } from "@yaakapp-internal/models";
|
import { pluginsAtom, settingsAtom } from "@yaakapp-internal/models";
|
||||||
import { HeaderSize, HStack, Icon } from "@yaakapp-internal/ui";
|
import { HeaderSize, HStack, Icon } from "@yaakapp-internal/ui";
|
||||||
@@ -21,6 +19,8 @@ import { SettingsProxy } from "./SettingsProxy";
|
|||||||
import { SettingsTheme } from "./SettingsTheme";
|
import { SettingsTheme } from "./SettingsTheme";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
tab?: SettingsTabWithSubtab | null;
|
||||||
|
/** Set when Settings is in a dialog rather than owning a window. */
|
||||||
hide?: () => void;
|
hide?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,25 +43,19 @@ const tabs = [
|
|||||||
TAB_LICENSE,
|
TAB_LICENSE,
|
||||||
] as const;
|
] as const;
|
||||||
export type SettingsTab = (typeof tabs)[number];
|
export type SettingsTab = (typeof tabs)[number];
|
||||||
|
export type SettingsTabWithSubtab = SettingsTab | `${SettingsTab}:${string}`;
|
||||||
|
|
||||||
export default function Settings({ hide }: Props) {
|
export default function Settings({ tab, hide }: Props) {
|
||||||
const { tab: tabFromQuery } = useSearch({ from: "/workspaces/$workspaceId/settings" });
|
|
||||||
// Parse tab and subtab (e.g., "plugins:installed")
|
// Parse tab and subtab (e.g., "plugins:installed")
|
||||||
const [mainTab, subtab] = tabFromQuery?.split(":") ?? [];
|
const [mainTab, subtab] = tab?.split(":") ?? [];
|
||||||
const settings = useAtomValue(settingsAtom);
|
const settings = useAtomValue(settingsAtom);
|
||||||
const plugins = useAtomValue(pluginsAtom);
|
const plugins = useAtomValue(pluginsAtom);
|
||||||
const licenseCheck = useLicense();
|
const licenseCheck = useLicense();
|
||||||
|
|
||||||
// Close settings window on escape
|
// Close settings window on escape. In a dialog, the dialog handles Escape itself.
|
||||||
// TODO: Could this be put in a better place? Eg. in Rust key listener when creating the window
|
// TODO: Could this be put in a better place? Eg. in Rust key listener when creating the window
|
||||||
useKeyPressEvent("Escape", async () => {
|
useKeyPressEvent("Escape", async () => {
|
||||||
if (hide != null) {
|
if (hide == null) await platform.window.close();
|
||||||
// It's being shown in a dialog, so close the dialog
|
|
||||||
hide();
|
|
||||||
} else {
|
|
||||||
// It's being shown in a window, so close the window
|
|
||||||
await getCurrentWebviewWindow().close();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -75,7 +69,7 @@ export default function Settings({ hide }: Props) {
|
|||||||
onlyXWindowControl
|
onlyXWindowControl
|
||||||
size="md"
|
size="md"
|
||||||
className="x-theme-appHeader bg-surface text-text-subtle flex items-center justify-center border-b border-border-subtle text-sm font-semibold"
|
className="x-theme-appHeader bg-surface text-text-subtle flex items-center justify-center border-b border-border-subtle text-sm font-semibold"
|
||||||
osType={type()}
|
osType={platform.osType()}
|
||||||
hideWindowControls={settings.hideWindowControls}
|
hideWindowControls={settings.hideWindowControls}
|
||||||
useNativeTitlebar={settings.useNativeTitlebar}
|
useNativeTitlebar={settings.useNativeTitlebar}
|
||||||
interfaceScale={settings.interfaceScale}
|
interfaceScale={settings.interfaceScale}
|
||||||
@@ -85,13 +79,13 @@ export default function Settings({ hide }: Props) {
|
|||||||
justifyContent="center"
|
justifyContent="center"
|
||||||
className="w-full h-full grid grid-cols-[1fr_auto] pointer-events-none"
|
className="w-full h-full grid grid-cols-[1fr_auto] pointer-events-none"
|
||||||
>
|
>
|
||||||
<div className={classNames(type() === "macos" ? "text-center" : "pl-2")}>Settings</div>
|
<div className={classNames(platform.osType() === "macos" ? "text-center" : "pl-2")}>Settings</div>
|
||||||
</HStack>
|
</HStack>
|
||||||
</HeaderSize>
|
</HeaderSize>
|
||||||
)}
|
)}
|
||||||
<Tabs
|
<Tabs
|
||||||
layout="horizontal"
|
layout="horizontal"
|
||||||
defaultValue={mainTab || tabFromQuery}
|
defaultValue={mainTab}
|
||||||
addBorders
|
addBorders
|
||||||
tabListClassName="min-w-40 bg-surface x-theme-sidebar border-r border-border pl-3"
|
tabListClassName="min-w-40 bg-surface x-theme-sidebar border-r border-border pl-3"
|
||||||
label="Settings"
|
label="Settings"
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { revealItemInDir } from "@tauri-apps/plugin-opener";
|
|
||||||
import { patchModel, settingsAtom } from "@yaakapp-internal/models";
|
import { patchModel, settingsAtom } from "@yaakapp-internal/models";
|
||||||
import { Heading, VStack } from "@yaakapp-internal/ui";
|
import { Heading, VStack } from "@yaakapp-internal/ui";
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
@@ -9,6 +8,7 @@ import { CargoFeature } from "../CargoFeature";
|
|||||||
import { CommercialUseBanner } from "../CommercialUseBanner";
|
import { CommercialUseBanner } from "../CommercialUseBanner";
|
||||||
import { DismissibleBanner } from "../core/DismissibleBanner";
|
import { DismissibleBanner } from "../core/DismissibleBanner";
|
||||||
import { IconButton } from "../core/IconButton";
|
import { IconButton } from "../core/IconButton";
|
||||||
|
import { platform } from "@yaakapp-internal/platform";
|
||||||
import {
|
import {
|
||||||
ModelSettingRowBoolean,
|
ModelSettingRowBoolean,
|
||||||
ModelSettingSelectControl,
|
ModelSettingSelectControl,
|
||||||
@@ -152,7 +152,7 @@ export function SettingsGeneral() {
|
|||||||
{
|
{
|
||||||
title: revealInFinderText,
|
title: revealInFinderText,
|
||||||
icon: "folder_open",
|
icon: "folder_open",
|
||||||
onClick: () => revealItemInDir(appInfo.appDataDir),
|
onClick: () => platform.revealItemInDir(appInfo.appDataDir),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
@@ -168,7 +168,7 @@ export function SettingsGeneral() {
|
|||||||
{
|
{
|
||||||
title: revealInFinderText,
|
title: revealInFinderText,
|
||||||
icon: "folder_open",
|
icon: "folder_open",
|
||||||
onClick: () => revealItemInDir(appInfo.appLogDir),
|
onClick: () => platform.revealItemInDir(appInfo.appLogDir),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ export function SettingsHotkeys() {
|
|||||||
<HotkeyRow
|
<HotkeyRow
|
||||||
key={action}
|
key={action}
|
||||||
action={action}
|
action={action}
|
||||||
currentKeys={hotkeys[action]}
|
currentKeys={hotkeys[action] ?? []}
|
||||||
defaultKeys={defaultHotkeys[action]}
|
defaultKeys={defaultHotkeys[action]}
|
||||||
onSave={async (keys) => {
|
onSave={async (keys) => {
|
||||||
const newHotkeys = { ...settings.hotkeys };
|
const newHotkeys = { ...settings.hotkeys };
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { type } from "@tauri-apps/plugin-os";
|
import { platform } from "@yaakapp-internal/platform";
|
||||||
import { useFonts } from "@yaakapp-internal/fonts";
|
import { useFonts } from "@yaakapp-internal/fonts";
|
||||||
import { useLicense } from "@yaakapp-internal/license";
|
import { useLicense } from "@yaakapp-internal/license";
|
||||||
import type { EditorKeymap, Settings } from "@yaakapp-internal/models";
|
import type { EditorKeymap, Settings } from "@yaakapp-internal/models";
|
||||||
@@ -9,7 +9,7 @@ import { useState } from "react";
|
|||||||
import { activeWorkspaceAtom } from "../../hooks/useActiveWorkspace";
|
import { activeWorkspaceAtom } from "../../hooks/useActiveWorkspace";
|
||||||
import { showConfirm } from "../../lib/confirm";
|
import { showConfirm } from "../../lib/confirm";
|
||||||
import { pricingUrl } from "../../lib/pricingUrl";
|
import { pricingUrl } from "../../lib/pricingUrl";
|
||||||
import { invokeCmd } from "../../lib/tauri";
|
import { rpc } from "../../lib/rpc";
|
||||||
import { CargoFeature } from "../CargoFeature";
|
import { CargoFeature } from "../CargoFeature";
|
||||||
import { Button } from "../core/Button";
|
import { Button } from "../core/Button";
|
||||||
import { Checkbox } from "../core/Checkbox";
|
import { Checkbox } from "../core/Checkbox";
|
||||||
@@ -176,7 +176,7 @@ export function SettingsInterface() {
|
|||||||
|
|
||||||
<SettingsSection title="Window">
|
<SettingsSection title="Window">
|
||||||
<NativeTitlebarSetting settings={settings} />
|
<NativeTitlebarSetting settings={settings} />
|
||||||
{type() !== "macos" && (
|
{platform.osType() !== "macos" && (
|
||||||
<ModelSettingRowBoolean
|
<ModelSettingRowBoolean
|
||||||
model={settings}
|
model={settings}
|
||||||
modelKey="hideWindowControls"
|
modelKey="hideWindowControls"
|
||||||
@@ -216,7 +216,7 @@ function NativeTitlebarSetting({ settings }: { settings: Settings }) {
|
|||||||
size="xs"
|
size="xs"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await patchModel(settings, { useNativeTitlebar: nativeTitlebar });
|
await patchModel(settings, { useNativeTitlebar: nativeTitlebar });
|
||||||
await invokeCmd("cmd_restart");
|
await rpc("cmd_restart");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Apply and Restart
|
Apply and Restart
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { openUrl } from "@tauri-apps/plugin-opener";
|
|
||||||
import { useLicense } from "@yaakapp-internal/license";
|
import { useLicense } from "@yaakapp-internal/license";
|
||||||
import { Banner, HStack, Icon, VStack } from "@yaakapp-internal/ui";
|
import { Banner, HStack, Icon, VStack } from "@yaakapp-internal/ui";
|
||||||
import { differenceInDays } from "date-fns";
|
import { differenceInDays } from "date-fns";
|
||||||
@@ -12,6 +11,7 @@ import { Button } from "../core/Button";
|
|||||||
import { Link } from "../core/Link";
|
import { Link } from "../core/Link";
|
||||||
import { PlainInput } from "../core/PlainInput";
|
import { PlainInput } from "../core/PlainInput";
|
||||||
import { Separator } from "../core/Separator";
|
import { Separator } from "../core/Separator";
|
||||||
|
import { platform } from "@yaakapp-internal/platform";
|
||||||
|
|
||||||
export function SettingsLicense() {
|
export function SettingsLicense() {
|
||||||
return (
|
return (
|
||||||
@@ -135,7 +135,7 @@ function SettingsLicenseCmp() {
|
|||||||
<Button
|
<Button
|
||||||
color="secondary"
|
color="secondary"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => openUrl("https://yaak.app/dashboard?intent=app.license.support")}
|
onClick={() => platform.openUrl("https://yaak.app/dashboard?intent=app.license.support")}
|
||||||
rightSlot={<Icon icon="external_link" />}
|
rightSlot={<Icon icon="external_link" />}
|
||||||
>
|
>
|
||||||
Direct Support
|
Direct Support
|
||||||
@@ -151,7 +151,7 @@ function SettingsLicenseCmp() {
|
|||||||
color="primary"
|
color="primary"
|
||||||
rightSlot={<Icon icon="external_link" />}
|
rightSlot={<Icon icon="external_link" />}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
openUrl(pricingUrl(`app.license.purchase.${check.data?.status ?? "unknown"}`))
|
platform.openUrl(pricingUrl(`app.license.purchase.${check.data?.status ?? "unknown"}`))
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Purchase License
|
Purchase License
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
import { openUrl } from "@tauri-apps/plugin-opener";
|
|
||||||
import type { Plugin } from "@yaakapp-internal/models";
|
import type { Plugin } from "@yaakapp-internal/models";
|
||||||
import { patchModel, pluginsAtom } from "@yaakapp-internal/models";
|
import { patchModel, pluginsAtom } from "@yaakapp-internal/models";
|
||||||
import type { PluginVersion } from "@yaakapp-internal/plugins";
|
import type { PluginVersion } from "@yaakapp-internal/plugins";
|
||||||
@@ -39,6 +38,7 @@ import { PlainInput } from "../core/PlainInput";
|
|||||||
import { TabContent, Tabs } from "../core/Tabs/Tabs";
|
import { TabContent, Tabs } from "../core/Tabs/Tabs";
|
||||||
import { EmptyStateText } from "../EmptyStateText";
|
import { EmptyStateText } from "../EmptyStateText";
|
||||||
import { SelectFile } from "../SelectFile";
|
import { SelectFile } from "../SelectFile";
|
||||||
|
import { platform } from "@yaakapp-internal/platform";
|
||||||
|
|
||||||
interface SettingsPluginsProps {
|
interface SettingsPluginsProps {
|
||||||
defaultSubtab?: string;
|
defaultSubtab?: string;
|
||||||
@@ -113,7 +113,7 @@ export function SettingsPlugins({ defaultSubtab }: SettingsPluginsProps) {
|
|||||||
icon="help"
|
icon="help"
|
||||||
title="View documentation"
|
title="View documentation"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
openUrl("https://yaak.app/docs/plugin-development/plugins-quick-start")
|
platform.openUrl("https://yaak.app/docs/plugin-development/plugins-quick-start")
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { openUrl } from "@tauri-apps/plugin-opener";
|
|
||||||
import { useLicense } from "@yaakapp-internal/license";
|
import { useLicense } from "@yaakapp-internal/license";
|
||||||
import { useRef } from "react";
|
import { useRef } from "react";
|
||||||
import { openSettings } from "../commands/openSettings";
|
import { openSettings } from "../commands/openSettings";
|
||||||
@@ -13,6 +12,7 @@ import { Dropdown } from "./core/Dropdown";
|
|||||||
import { Icon } from "@yaakapp-internal/ui";
|
import { Icon } from "@yaakapp-internal/ui";
|
||||||
import { IconButton } from "./core/IconButton";
|
import { IconButton } from "./core/IconButton";
|
||||||
import { KeyboardShortcutsDialog } from "./KeyboardShortcutsDialog";
|
import { KeyboardShortcutsDialog } from "./KeyboardShortcutsDialog";
|
||||||
|
import { platform } from "@yaakapp-internal/platform";
|
||||||
|
|
||||||
export function SettingsDropdown() {
|
export function SettingsDropdown() {
|
||||||
const exportData = useExportData();
|
const exportData = useExportData();
|
||||||
@@ -62,7 +62,7 @@ export function SettingsDropdown() {
|
|||||||
{
|
{
|
||||||
label: "Create Run Button",
|
label: "Create Run Button",
|
||||||
leftSlot: <Icon icon="rocket" />,
|
leftSlot: <Icon icon="rocket" />,
|
||||||
onSelect: () => openUrl("https://yaak.app/button/new"),
|
onSelect: () => platform.openUrl("https://yaak.app/button/new"),
|
||||||
},
|
},
|
||||||
{ type: "separator", label: `Yaak v${appInfo.version}` },
|
{ type: "separator", label: `Yaak v${appInfo.version}` },
|
||||||
{
|
{
|
||||||
@@ -78,26 +78,26 @@ export function SettingsDropdown() {
|
|||||||
leftSlot: <Icon icon="circle_dollar_sign" />,
|
leftSlot: <Icon icon="circle_dollar_sign" />,
|
||||||
rightSlot: <Icon icon="external_link" color="success" className="opacity-60" />,
|
rightSlot: <Icon icon="external_link" color="success" className="opacity-60" />,
|
||||||
onSelect: () =>
|
onSelect: () =>
|
||||||
openUrl(pricingUrl(`app.menu.purchase.${check.data?.status ?? "unknown"}`)),
|
platform.openUrl(pricingUrl(`app.menu.purchase.${check.data?.status ?? "unknown"}`)),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Install CLI",
|
label: "Install CLI",
|
||||||
hidden: appInfo.cliVersion != null,
|
hidden: appInfo.cliVersion != null,
|
||||||
leftSlot: <Icon icon="square_terminal" />,
|
leftSlot: <Icon icon="square_terminal" />,
|
||||||
rightSlot: <Icon icon="external_link" color="secondary" />,
|
rightSlot: <Icon icon="external_link" color="secondary" />,
|
||||||
onSelect: () => openUrl("https://yaak.app/docs/cli"),
|
onSelect: () => platform.openUrl("https://yaak.app/docs/cli"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Feedback",
|
label: "Feedback",
|
||||||
leftSlot: <Icon icon="chat" />,
|
leftSlot: <Icon icon="chat" />,
|
||||||
rightSlot: <Icon icon="external_link" color="secondary" />,
|
rightSlot: <Icon icon="external_link" color="secondary" />,
|
||||||
onSelect: () => openUrl("https://yaak.app/feedback"),
|
onSelect: () => platform.openUrl("https://yaak.app/feedback"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Changelog",
|
label: "Changelog",
|
||||||
leftSlot: <Icon icon="cake" />,
|
leftSlot: <Icon icon="cake" />,
|
||||||
rightSlot: <Icon icon="external_link" color="secondary" />,
|
rightSlot: <Icon icon="external_link" color="secondary" />,
|
||||||
onSelect: () => openUrl(`https://yaak.app/changelog/${appInfo.version}`),
|
onSelect: () => platform.openUrl(`https://yaak.app/changelog/${appInfo.version}`),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import type { Extension } from "@codemirror/state";
|
import type { Extension } from "@codemirror/state";
|
||||||
import { Compartment } from "@codemirror/state";
|
import { Compartment } from "@codemirror/state";
|
||||||
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
|
|
||||||
import { debounce } from "@yaakapp-internal/lib";
|
import { debounce } from "@yaakapp-internal/lib";
|
||||||
import { gitMutations } from "@yaakapp-internal/git";
|
import { gitMutations } from "@yaakapp-internal/git";
|
||||||
import type { GitStatus } from "@yaakapp-internal/git";
|
import type { GitStatus } from "@yaakapp-internal/git";
|
||||||
@@ -44,7 +43,8 @@ import { getFolderActions } from "../hooks/useFolderActions";
|
|||||||
import { getGrpcRequestActions } from "../hooks/useGrpcRequestActions";
|
import { getGrpcRequestActions } from "../hooks/useGrpcRequestActions";
|
||||||
import { useHotKey } from "../hooks/useHotKey";
|
import { useHotKey } from "../hooks/useHotKey";
|
||||||
import { getHttpRequestActions } from "../hooks/useHttpRequestActions";
|
import { getHttpRequestActions } from "../hooks/useHttpRequestActions";
|
||||||
import { useListenToTauriEvent } from "../hooks/useListenToTauriEvent";
|
import { platform } from "@yaakapp-internal/platform";
|
||||||
|
import { usePlatformEvent } from "../hooks/usePlatformEvent";
|
||||||
import { getModelAncestors } from "../hooks/useModelAncestors";
|
import { getModelAncestors } from "../hooks/useModelAncestors";
|
||||||
import { sendAnyHttpRequest } from "../hooks/useSendAnyHttpRequest";
|
import { sendAnyHttpRequest } from "../hooks/useSendAnyHttpRequest";
|
||||||
import { useSidebarHidden } from "../hooks/useSidebarHidden";
|
import { useSidebarHidden } from "../hooks/useSidebarHidden";
|
||||||
@@ -135,10 +135,10 @@ function Sidebar({ className }: { className?: string }) {
|
|||||||
// Focus new sidebar models created by the user in this window. Writes from other
|
// Focus new sidebar models created by the user in this window. Writes from other
|
||||||
// sources (import, sync, CLI) can carry thousands of models and shouldn't move
|
// sources (import, sync, CLI) can carry thousands of models and shouldn't move
|
||||||
// the selection.
|
// the selection.
|
||||||
useListenToTauriEvent<ModelPayload[]>("model_writes", ({ payload: payloads }) => {
|
usePlatformEvent<ModelPayload[]>("model_writes", (payloads) => {
|
||||||
for (const payload of payloads) {
|
for (const payload of payloads) {
|
||||||
if (payload.updateSource.type !== "window") continue;
|
if (payload.updateSource.type !== "window") continue;
|
||||||
if (payload.updateSource.label !== getCurrentWebviewWindow().label) continue;
|
if (payload.updateSource.label !== platform.window.label) continue;
|
||||||
if (!isSidebarLeafModel(payload.model)) continue;
|
if (!isSidebarLeafModel(payload.model)) continue;
|
||||||
if (!(payload.change.type === "upsert" && payload.change.created)) continue;
|
if (!(payload.change.type === "upsert" && payload.change.created)) continue;
|
||||||
treeRef.current?.selectItem(payload.model.id, true);
|
treeRef.current?.selectItem(payload.model.id, true);
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { readDir } from "@tauri-apps/plugin-fs";
|
|
||||||
import { Banner, VStack } from "@yaakapp-internal/ui";
|
import { Banner, VStack } from "@yaakapp-internal/ui";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { openWorkspaceFromSyncDir } from "../commands/openWorkspaceFromSyncDir";
|
import { openWorkspaceFromSyncDir } from "../commands/openWorkspaceFromSyncDir";
|
||||||
@@ -6,6 +5,7 @@ import { Button } from "./core/Button";
|
|||||||
import { Checkbox } from "./core/Checkbox";
|
import { Checkbox } from "./core/Checkbox";
|
||||||
import { SettingRowBoolean, SettingRowDirectory } from "./core/SettingRow";
|
import { SettingRowBoolean, SettingRowDirectory } from "./core/SettingRow";
|
||||||
import { SelectFile } from "./SelectFile";
|
import { SelectFile } from "./SelectFile";
|
||||||
|
import { platform } from "@yaakapp-internal/platform";
|
||||||
|
|
||||||
export interface SyncToFilesystemSettingProps {
|
export interface SyncToFilesystemSettingProps {
|
||||||
layout?: "form" | "settings";
|
layout?: "form" | "settings";
|
||||||
@@ -24,7 +24,7 @@ export function SyncToFilesystemSetting({
|
|||||||
|
|
||||||
const handleFilePathChange = async (filePath: string | null) => {
|
const handleFilePathChange = async (filePath: string | null) => {
|
||||||
if (filePath != null) {
|
if (filePath != null) {
|
||||||
const files = await readDir(filePath);
|
const files = await platform.files.readDir(filePath);
|
||||||
if (files.length > 0) {
|
if (files.length > 0) {
|
||||||
setSyncDir(filePath);
|
setSyncDir(filePath);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { type } from "@tauri-apps/plugin-os";
|
import { platform } from "@yaakapp-internal/platform";
|
||||||
import { settingsAtom, workspacesAtom } from "@yaakapp-internal/models";
|
import { settingsAtom, workspacesAtom } from "@yaakapp-internal/models";
|
||||||
import { Banner, HeaderSize, HStack, SidebarLayout } from "@yaakapp-internal/ui";
|
import { Banner, HeaderSize, HStack, SidebarLayout } from "@yaakapp-internal/ui";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
@@ -53,7 +53,7 @@ export function Workspace() {
|
|||||||
|
|
||||||
const workspaces = useAtomValue(workspacesAtom);
|
const workspaces = useAtomValue(workspacesAtom);
|
||||||
const settings = useAtomValue(settingsAtom);
|
const settings = useAtomValue(settingsAtom);
|
||||||
const osType = type();
|
const osType = platform.osType();
|
||||||
const [width, setWidth] = useSidebarWidth();
|
const [width, setWidth] = useSidebarWidth();
|
||||||
const [sidebarHidden, setSidebarHidden] = useSidebarHidden();
|
const [sidebarHidden, setSidebarHidden] = useSidebarHidden();
|
||||||
const [floatingSidebarHidden, setFloatingSidebarHidden] = useFloatingSidebarHidden();
|
const [floatingSidebarHidden, setFloatingSidebarHidden] = useFloatingSidebarHidden();
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { open } from "@tauri-apps/plugin-dialog";
|
|
||||||
import { revealItemInDir } from "@tauri-apps/plugin-opener";
|
|
||||||
import { getModel, settingsAtom, workspacesAtom } from "@yaakapp-internal/models";
|
import { getModel, settingsAtom, workspacesAtom } from "@yaakapp-internal/models";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
@@ -27,6 +25,7 @@ import { Icon } from "@yaakapp-internal/ui";
|
|||||||
import type { RadioDropdownItem } from "./core/RadioDropdown";
|
import type { RadioDropdownItem } from "./core/RadioDropdown";
|
||||||
import { RadioDropdown } from "./core/RadioDropdown";
|
import { RadioDropdown } from "./core/RadioDropdown";
|
||||||
import { SwitchWorkspaceDialog } from "./SwitchWorkspaceDialog";
|
import { SwitchWorkspaceDialog } from "./SwitchWorkspaceDialog";
|
||||||
|
import { platform } from "@yaakapp-internal/platform";
|
||||||
|
|
||||||
type Props = Pick<ButtonProps, "className" | "justify" | "forDropdown" | "leftSlot">;
|
type Props = Pick<ButtonProps, "className" | "justify" | "forDropdown" | "leftSlot">;
|
||||||
|
|
||||||
@@ -76,7 +75,7 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
|
|||||||
label: "Open Folder",
|
label: "Open Folder",
|
||||||
leftSlot: <Icon icon="folder_open" />,
|
leftSlot: <Icon icon="folder_open" />,
|
||||||
onSelect: async () => {
|
onSelect: async () => {
|
||||||
const dir = await open({
|
const dir = await platform.dialog.open({
|
||||||
title: "Select Workspace Directory",
|
title: "Select Workspace Directory",
|
||||||
directory: true,
|
directory: true,
|
||||||
multiple: false,
|
multiple: false,
|
||||||
@@ -120,7 +119,7 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
|
|||||||
leftSlot: <Icon icon="folder_symlink" />,
|
leftSlot: <Icon icon="folder_symlink" />,
|
||||||
onSelect: async () => {
|
onSelect: async () => {
|
||||||
if (workspaceMeta?.settingSyncDir == null) return;
|
if (workspaceMeta?.settingSyncDir == null) return;
|
||||||
await revealItemInDir(workspaceMeta.settingSyncDir);
|
await platform.revealItemInDir(workspaceMeta.settingSyncDir);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { CookieDropdown } from "./CookieDropdown";
|
|||||||
import { IconButton } from "./core/IconButton";
|
import { IconButton } from "./core/IconButton";
|
||||||
import { PillButton } from "./core/PillButton";
|
import { PillButton } from "./core/PillButton";
|
||||||
import { EnvironmentActionsDropdown } from "./EnvironmentActionsDropdown";
|
import { EnvironmentActionsDropdown } from "./EnvironmentActionsDropdown";
|
||||||
import { ImportCurlButton } from "./ImportCurlButton";
|
import { ImportCurl } from "./ImportCurl";
|
||||||
import { LicenseBadge } from "./LicenseBadge";
|
import { LicenseBadge } from "./LicenseBadge";
|
||||||
import { RecentRequestsDropdown } from "./RecentRequestsDropdown";
|
import { RecentRequestsDropdown } from "./RecentRequestsDropdown";
|
||||||
import { SettingsDropdown } from "./SettingsDropdown";
|
import { SettingsDropdown } from "./SettingsDropdown";
|
||||||
@@ -56,7 +56,7 @@ export const WorkspaceHeader = memo(function WorkspaceHeader({
|
|||||||
<RecentRequestsDropdown />
|
<RecentRequestsDropdown />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 flex gap-1 items-center h-full justify-end pointer-events-none pr-1">
|
<div className="flex-1 flex gap-1 items-center h-full justify-end pointer-events-none pr-1">
|
||||||
<ImportCurlButton />
|
<ImportCurl />
|
||||||
{showEncryptionSetup ? (
|
{showEncryptionSetup ? (
|
||||||
<PillButton color="danger" onClick={setupOrConfigureEncryption}>
|
<PillButton color="danger" onClick={setupOrConfigureEncryption}>
|
||||||
Enter Encryption Key
|
Enter Encryption Key
|
||||||
|
|||||||
@@ -36,12 +36,16 @@ import { fireAndForget } from "../../lib/fireAndForget";
|
|||||||
import { ErrorBoundary } from "../ErrorBoundary";
|
import { ErrorBoundary } from "../ErrorBoundary";
|
||||||
import { Button } from "./Button";
|
import { Button } from "./Button";
|
||||||
import { Hotkey } from "./Hotkey";
|
import { Hotkey } from "./Hotkey";
|
||||||
|
import { IconButton } from "./IconButton";
|
||||||
|
import type { SeparatorAction } from "./Separator";
|
||||||
import { Separator } from "./Separator";
|
import { Separator } from "./Separator";
|
||||||
|
|
||||||
export type DropdownItemSeparator = {
|
export type DropdownItemSeparator = {
|
||||||
type: "separator";
|
type: "separator";
|
||||||
label?: ReactNode;
|
label?: ReactNode;
|
||||||
hidden?: boolean;
|
hidden?: boolean;
|
||||||
|
/** A control shown beside the label, eg. revealing the labelled file on disk. */
|
||||||
|
action?: SeparatorAction;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DropdownItemContent = {
|
export type DropdownItemContent = {
|
||||||
@@ -66,6 +70,12 @@ export type DropdownItemDefault = {
|
|||||||
submenu?: DropdownItem[];
|
submenu?: DropdownItem[];
|
||||||
/** If true, submenu opens on click instead of hover */
|
/** If true, submenu opens on click instead of hover */
|
||||||
submenuOpenOnClick?: boolean;
|
submenuOpenOnClick?: boolean;
|
||||||
|
/**
|
||||||
|
* How the submenu opens. "row" (default) opens it from the row itself (hover, or click
|
||||||
|
* with submenuOpenOnClick). "button" keeps the row selectable via onSelect and renders
|
||||||
|
* a dedicated button on the right that opens the submenu.
|
||||||
|
*/
|
||||||
|
submenuTrigger?: "row" | "button";
|
||||||
icon?: IconProps["icon"];
|
icon?: IconProps["icon"];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -502,9 +512,15 @@ const Menu = forwardRef<Omit<DropdownRef, "open" | "isOpen" | "toggle" | "items"
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!item.keepOpenOnSelect) handleCloseAll();
|
if (!item.keepOpenOnSelect) {
|
||||||
|
handleCloseAll();
|
||||||
|
} else if (isSubmenu) {
|
||||||
|
// Keep the parent menu open, but close this submenu — its items may no
|
||||||
|
// longer describe the row after the action (e.g. Pin → Unpin, Remove)
|
||||||
|
handleClose();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[handleCloseAll, setSelectedIndex],
|
[handleCloseAll, handleClose, isSubmenu, setSelectedIndex],
|
||||||
);
|
);
|
||||||
|
|
||||||
useImperativeHandle(ref, () => {
|
useImperativeHandle(ref, () => {
|
||||||
@@ -629,7 +645,7 @@ const Menu = forwardRef<Omit<DropdownRef, "open" | "isOpen" | "toggle" | "items"
|
|||||||
const item = filteredItems[selectedIndex ?? -1];
|
const item = filteredItems[selectedIndex ?? -1];
|
||||||
if (!item || item.type === "separator" || item.type === "content") return;
|
if (!item || item.type === "separator" || item.type === "content") return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (item.submenu) {
|
if (item.submenu && item.submenuTrigger !== "button") {
|
||||||
const parent = document.activeElement as HTMLButtonElement;
|
const parent = document.activeElement as HTMLButtonElement;
|
||||||
if (parent) {
|
if (parent) {
|
||||||
setActiveSubmenu({ item, parent, viaKeyboard: true });
|
setActiveSubmenu({ item, parent, viaKeyboard: true });
|
||||||
@@ -648,9 +664,11 @@ const Menu = forwardRef<Omit<DropdownRef, "open" | "isOpen" | "toggle" | "items"
|
|||||||
clearTimeout(submenuTimeoutRef.current);
|
clearTimeout(submenuTimeoutRef.current);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.submenu && !item.submenuOpenOnClick) {
|
if (item.submenu && !item.submenuOpenOnClick && item.submenuTrigger !== "button") {
|
||||||
setActiveSubmenu({ item, parent });
|
setActiveSubmenu({ item, parent });
|
||||||
} else if (activeSubmenu) {
|
} else if (activeSubmenu && activeSubmenu.item !== item) {
|
||||||
|
// Hovering the row that owns the open submenu must not dismiss it — the
|
||||||
|
// pointer travels across the row on its way to a button-triggered submenu
|
||||||
submenuTimeoutRef.current = window.setTimeout(() => {
|
submenuTimeoutRef.current = window.setTimeout(() => {
|
||||||
const submenuEl = submenuRef.current;
|
const submenuEl = submenuRef.current;
|
||||||
if (!submenuEl || !activeSubmenu) {
|
if (!submenuEl || !activeSubmenu) {
|
||||||
@@ -776,6 +794,7 @@ const Menu = forwardRef<Omit<DropdownRef, "open" | "isOpen" | "toggle" | "items"
|
|||||||
// oxlint-disable-next-line no-array-index-key -- Nothing else available
|
// oxlint-disable-next-line no-array-index-key -- Nothing else available
|
||||||
key={i}
|
key={i}
|
||||||
className={classNames("my-1.5", item.label ? "ml-2" : null)}
|
className={classNames("my-1.5", item.label ? "ml-2" : null)}
|
||||||
|
action={item.action}
|
||||||
>
|
>
|
||||||
{item.label}
|
{item.label}
|
||||||
</Separator>
|
</Separator>
|
||||||
@@ -797,6 +816,7 @@ const Menu = forwardRef<Omit<DropdownRef, "open" | "isOpen" | "toggle" | "items"
|
|||||||
onFocus={handleFocus}
|
onFocus={handleFocus}
|
||||||
onSelect={handleSelect}
|
onSelect={handleSelect}
|
||||||
onHover={handleItemHover}
|
onHover={handleItemHover}
|
||||||
|
onOpenSubmenu={(item, el) => setActiveSubmenu({ item, parent: el })}
|
||||||
// oxlint-disable-next-line no-array-index-key -- It's fine
|
// oxlint-disable-next-line no-array-index-key -- It's fine
|
||||||
key={i}
|
key={i}
|
||||||
item={item}
|
item={item}
|
||||||
@@ -868,6 +888,7 @@ interface MenuItemProps {
|
|||||||
onSelect: (item: DropdownItemDefault, el?: HTMLButtonElement) => Promise<void>;
|
onSelect: (item: DropdownItemDefault, el?: HTMLButtonElement) => Promise<void>;
|
||||||
onFocus: (item: DropdownItemDefault) => void;
|
onFocus: (item: DropdownItemDefault) => void;
|
||||||
onHover: (item: DropdownItemDefault, el: HTMLButtonElement) => void;
|
onHover: (item: DropdownItemDefault, el: HTMLButtonElement) => void;
|
||||||
|
onOpenSubmenu: (item: DropdownItemDefault, el: HTMLButtonElement) => void;
|
||||||
focused: boolean;
|
focused: boolean;
|
||||||
isParentOfActiveSubmenu?: boolean;
|
isParentOfActiveSubmenu?: boolean;
|
||||||
}
|
}
|
||||||
@@ -879,6 +900,7 @@ function MenuItem({
|
|||||||
onHover,
|
onHover,
|
||||||
item,
|
item,
|
||||||
onSelect,
|
onSelect,
|
||||||
|
onOpenSubmenu,
|
||||||
isParentOfActiveSubmenu,
|
isParentOfActiveSubmenu,
|
||||||
...props
|
...props
|
||||||
}: MenuItemProps) {
|
}: MenuItemProps) {
|
||||||
@@ -914,19 +936,22 @@ function MenuItem({
|
|||||||
e.currentTarget.focus();
|
e.currentTarget.focus();
|
||||||
};
|
};
|
||||||
|
|
||||||
const rightSlot = item.submenu ? (
|
const hasButtonSubmenu = item.submenu != null && item.submenuTrigger === "button";
|
||||||
|
|
||||||
|
const rightSlot =
|
||||||
|
item.submenu && !hasButtonSubmenu ? (
|
||||||
<Icon icon="chevron_right" color="secondary" />
|
<Icon icon="chevron_right" color="secondary" />
|
||||||
) : (
|
) : (
|
||||||
(item.rightSlot ?? <Hotkey variant="text" action={item.hotKeyAction ?? null} />)
|
(item.rightSlot ?? <Hotkey variant="text" action={item.hotKeyAction ?? null} />)
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
const button = (
|
||||||
<Button
|
<Button
|
||||||
ref={initRef}
|
ref={initRef}
|
||||||
size="sm"
|
size="sm"
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
onMouseEnter={handleMouseEnter}
|
onMouseEnter={hasButtonSubmenu ? undefined : handleMouseEnter}
|
||||||
onMouseLeave={(e) => e.currentTarget.blur()}
|
onMouseLeave={hasButtonSubmenu ? undefined : (e) => e.currentTarget.blur()}
|
||||||
disabled={item.disabled}
|
disabled={item.disabled}
|
||||||
onFocus={handleFocus}
|
onFocus={handleFocus}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
@@ -947,6 +972,7 @@ function MenuItem({
|
|||||||
"min-w-32 outline-hidden px-2 mx-1.5 flex whitespace-nowrap",
|
"min-w-32 outline-hidden px-2 mx-1.5 flex whitespace-nowrap",
|
||||||
"focus:bg-surface-highlight focus:text rounded-sm focus:outline-hidden focus-visible:outline-1",
|
"focus:bg-surface-highlight focus:text rounded-sm focus:outline-hidden focus-visible:outline-1",
|
||||||
isParentOfActiveSubmenu && "bg-surface-highlight text rounded-sm",
|
isParentOfActiveSubmenu && "bg-surface-highlight text rounded-sm",
|
||||||
|
hasButtonSubmenu && "pr-8",
|
||||||
item.color === "danger" && "text-danger!",
|
item.color === "danger" && "text-danger!",
|
||||||
item.color === "primary" && "text-primary!",
|
item.color === "primary" && "text-primary!",
|
||||||
item.color === "success" && "text-success!",
|
item.color === "success" && "text-success!",
|
||||||
@@ -959,6 +985,52 @@ function MenuItem({
|
|||||||
<div className={classNames("truncate min-w-20")}>{item.label}</div>
|
<div className={classNames("truncate min-w-20")}>{item.label}</div>
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!hasButtonSubmenu) {
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The submenu trigger overlays the row as a sibling (not a child) because the row is
|
||||||
|
// itself a button and buttons cannot nest. Hover handling lives on this wrapper so the
|
||||||
|
// row keeps its focus highlight while the mouse is over the trigger.
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="relative grid group/menuitem"
|
||||||
|
onMouseEnter={() => {
|
||||||
|
const el = buttonRef.current;
|
||||||
|
if (el == null) return;
|
||||||
|
onHover(item, el);
|
||||||
|
el.focus();
|
||||||
|
}}
|
||||||
|
onMouseLeave={() => buttonRef.current?.blur()}
|
||||||
|
>
|
||||||
|
{button}
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
"absolute right-1.5 inset-y-0 flex items-center",
|
||||||
|
"opacity-0 group-hover/menuitem:opacity-100 group-focus-within/menuitem:opacity-100",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
color="custom"
|
||||||
|
size="2xs"
|
||||||
|
tabIndex={-1}
|
||||||
|
icon="ellipsis_vertical"
|
||||||
|
iconColor="secondary"
|
||||||
|
title="More actions"
|
||||||
|
className="h-full! w-7!"
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
// Prevent the trigger from stealing focus, which would unhighlight the row
|
||||||
|
e.preventDefault();
|
||||||
|
}}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onOpenSubmenu(item, e.currentTarget);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MenuItemHotKeyProps {
|
interface MenuItemHotKeyProps {
|
||||||
|
|||||||
@@ -1,9 +1,29 @@
|
|||||||
import type { Diagnostic } from "@codemirror/lint";
|
import type { Diagnostic } from "@codemirror/lint";
|
||||||
import type { EditorView } from "@codemirror/view";
|
import type { EditorView } from "@codemirror/view";
|
||||||
import { parse as jsonLintParse } from "@prantlf/jsonlint";
|
import { type ParseError, parse, printParseErrorCode } from "jsonc-parser";
|
||||||
|
|
||||||
const TEMPLATE_SYNTAX_REGEX = /\$\{\[[\s\S]*?]}/g;
|
const TEMPLATE_SYNTAX_REGEX = /\$\{\[[\s\S]*?]}/g;
|
||||||
|
|
||||||
|
// jsonc-parser reports error codes, so these are the words the editor shows for them
|
||||||
|
const MESSAGES: Record<string, string> = {
|
||||||
|
InvalidSymbol: "Invalid symbol",
|
||||||
|
InvalidNumberFormat: "Invalid number format",
|
||||||
|
PropertyNameExpected: "Property name expected",
|
||||||
|
ValueExpected: "Value expected",
|
||||||
|
ColonExpected: "Colon expected",
|
||||||
|
CommaExpected: "Comma expected",
|
||||||
|
CloseBraceExpected: "Closing brace expected",
|
||||||
|
CloseBracketExpected: "Closing bracket expected",
|
||||||
|
EndOfFileExpected: "End of file expected",
|
||||||
|
InvalidCommentToken: "Comments are not allowed",
|
||||||
|
UnexpectedEndOfComment: "Unexpected end of comment",
|
||||||
|
UnexpectedEndOfString: "Unexpected end of string",
|
||||||
|
UnexpectedEndOfNumber: "Unexpected end of number",
|
||||||
|
InvalidUnicode: "Invalid unicode sequence",
|
||||||
|
InvalidEscapeCharacter: "Invalid escape character",
|
||||||
|
InvalidCharacter: "Invalid character",
|
||||||
|
};
|
||||||
|
|
||||||
interface JsonLintOptions {
|
interface JsonLintOptions {
|
||||||
allowComments?: boolean;
|
allowComments?: boolean;
|
||||||
allowTrailingCommas?: boolean;
|
allowTrailingCommas?: boolean;
|
||||||
@@ -11,34 +31,28 @@ interface JsonLintOptions {
|
|||||||
|
|
||||||
export function jsonParseLinter(options?: JsonLintOptions) {
|
export function jsonParseLinter(options?: JsonLintOptions) {
|
||||||
return (view: EditorView): Diagnostic[] => {
|
return (view: EditorView): Diagnostic[] => {
|
||||||
try {
|
|
||||||
const doc = view.state.doc.toString();
|
const doc = view.state.doc.toString();
|
||||||
// We need lint to not break on stuff like {"foo:" ${[ ... ]}} so we'll replace all template
|
// We need lint to not break on stuff like {"foo:" ${[ ... ]}} so we'll replace all template
|
||||||
// syntax with repeating `1` characters, so it's valid JSON and the position is still correct.
|
// syntax with repeating `1` characters, so it's valid JSON and the position is still correct.
|
||||||
const escapedDoc = doc.replace(TEMPLATE_SYNTAX_REGEX, (m) => "1".repeat(m.length));
|
const escapedDoc = doc.replace(TEMPLATE_SYNTAX_REGEX, (m) => "1".repeat(m.length));
|
||||||
jsonLintParse(escapedDoc, {
|
|
||||||
mode: (options?.allowComments ?? true) ? "cjson" : "json",
|
|
||||||
ignoreTrailingCommas: options?.allowTrailingCommas ?? false,
|
|
||||||
});
|
|
||||||
// oxlint-disable-next-line no-explicit-any
|
|
||||||
} catch (err: any) {
|
|
||||||
if (!("location" in err)) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// const line = location?.start?.line;
|
const errors: ParseError[] = [];
|
||||||
// const column = location?.start?.column;
|
parse(escapedDoc, errors, {
|
||||||
if (err.location.start.offset) {
|
allowTrailingComma: options?.allowTrailingCommas ?? false,
|
||||||
|
disallowComments: !(options?.allowComments ?? true),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Later errors are mostly consequences of the first one, so only that one is shown
|
||||||
|
const error = errors[0];
|
||||||
|
if (error == null) return [];
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
from: err.location.start.offset,
|
from: error.offset,
|
||||||
to: err.location.start.offset,
|
to: error.offset + error.length,
|
||||||
severity: "error",
|
severity: "error",
|
||||||
message: err.message,
|
message: MESSAGES[printParseErrorCode(error.error)] ?? "Invalid JSON",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
useSensor,
|
useSensor,
|
||||||
useSensors,
|
useSensors,
|
||||||
} from "@dnd-kit/core";
|
} from "@dnd-kit/core";
|
||||||
import { basename } from "@tauri-apps/api/path";
|
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import type { WrappedEnvironmentVariable } from "../../hooks/useEnvironmentVariables";
|
import type { WrappedEnvironmentVariable } from "../../hooks/useEnvironmentVariables";
|
||||||
@@ -34,6 +33,7 @@ import { Input } from "./Input";
|
|||||||
import { ensurePairId } from "./PairEditor.util";
|
import { ensurePairId } from "./PairEditor.util";
|
||||||
import type { RadioDropdownItem } from "./RadioDropdown";
|
import type { RadioDropdownItem } from "./RadioDropdown";
|
||||||
import { RadioDropdown } from "./RadioDropdown";
|
import { RadioDropdown } from "./RadioDropdown";
|
||||||
|
import { platform } from "@yaakapp-internal/platform";
|
||||||
|
|
||||||
export interface PairEditorHandle {
|
export interface PairEditorHandle {
|
||||||
/**
|
/**
|
||||||
@@ -851,7 +851,7 @@ function FileActionsDropdown({
|
|||||||
leftSlot: <Icon icon="file_code" />,
|
leftSlot: <Icon icon="file_code" />,
|
||||||
onSelect: async () => {
|
onSelect: async () => {
|
||||||
console.log("PAIR", pair);
|
console.log("PAIR", pair);
|
||||||
const defaultFilename = await basename(pair.value ?? "");
|
const defaultFilename = await platform.files.basename(pair.value ?? "");
|
||||||
const filename = await showPrompt({
|
const filename = await showPrompt({
|
||||||
id: "filename",
|
id: "filename",
|
||||||
title: "Override Filename",
|
title: "Override Filename",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { type } from "@tauri-apps/plugin-os";
|
import { platform } from "@yaakapp-internal/platform";
|
||||||
import { HStack } from "@yaakapp-internal/ui";
|
import { HStack } from "@yaakapp-internal/ui";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import type { CSSProperties, ReactNode } from "react";
|
import type { CSSProperties, ReactNode } from "react";
|
||||||
@@ -66,7 +66,7 @@ export function Select<T extends string>({
|
|||||||
<Label htmlFor={id} visuallyHidden={hideLabel} className={labelClassName} help={help}>
|
<Label htmlFor={id} visuallyHidden={hideLabel} className={labelClassName} help={help}>
|
||||||
{label}
|
{label}
|
||||||
</Label>
|
</Label>
|
||||||
{type() === "macos" && !filterable ? (
|
{platform.osType() === "macos" && !filterable ? (
|
||||||
<HStack
|
<HStack
|
||||||
space={2}
|
space={2}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
|
|||||||
@@ -1,13 +1,31 @@
|
|||||||
import type { Color } from "@yaakapp-internal/plugins";
|
import type { Color } from "@yaakapp-internal/plugins";
|
||||||
|
import type { IconProps } from "@yaakapp-internal/ui";
|
||||||
|
import { IconButton } from "@yaakapp-internal/ui";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A single control attached to a labelled separator, rendered between the label
|
||||||
|
* and the rule.
|
||||||
|
*
|
||||||
|
* Declared rather than passed as a node so the separator keeps ownership of the
|
||||||
|
* things that are easy to get wrong by hand: matching the label's colour, and
|
||||||
|
* staying out of the rule's way when the label is long.
|
||||||
|
*/
|
||||||
|
export interface SeparatorAction {
|
||||||
|
icon: IconProps["icon"];
|
||||||
|
/** Tooltip and accessible name. Required — the control is icon-only. */
|
||||||
|
title: string;
|
||||||
|
onClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
orientation?: "horizontal" | "vertical";
|
orientation?: "horizontal" | "vertical";
|
||||||
dashed?: boolean;
|
dashed?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
color?: Color;
|
color?: Color;
|
||||||
|
action?: SeparatorAction;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Separator({
|
export function Separator({
|
||||||
@@ -16,15 +34,31 @@ export function Separator({
|
|||||||
dashed,
|
dashed,
|
||||||
orientation = "horizontal",
|
orientation = "horizontal",
|
||||||
children,
|
children,
|
||||||
|
action,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
return (
|
return (
|
||||||
<div role="presentation" className={classNames(className, "flex items-center w-full")}>
|
<div role="presentation" className={classNames(className, "flex items-center w-full")}>
|
||||||
{children && (
|
{children && (
|
||||||
<div className="text-sm text-text-subtlest mr-2 whitespace-nowrap">{children}</div>
|
<div className="text-sm text-text-subtlest mr-2 whitespace-nowrap">{children}</div>
|
||||||
)}
|
)}
|
||||||
|
{action && (
|
||||||
|
<IconButton
|
||||||
|
size="2xs"
|
||||||
|
iconSize="xs"
|
||||||
|
className="shrink-0 mr-2 -ml-1"
|
||||||
|
// Forced, because the button itself sets `text-text` at full strength.
|
||||||
|
iconClassName="text-text-subtlest!"
|
||||||
|
icon={action.icon}
|
||||||
|
title={action.title}
|
||||||
|
onClick={action.onClick}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"opacity-60",
|
"opacity-60",
|
||||||
|
// Keep a stub of the line visible no matter how long the label is —
|
||||||
|
// `w-full` alone gets squeezed to nothing by a wide label.
|
||||||
|
orientation === "horizontal" && "min-w-8",
|
||||||
color == null && "border-border",
|
color == null && "border-border",
|
||||||
color === "primary" && "border-primary",
|
color === "primary" && "border-primary",
|
||||||
color === "secondary" && "border-secondary",
|
color === "secondary" && "border-secondary",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { HttpRequest } from "@yaakapp-internal/models";
|
import type { HttpRequest } from "@yaakapp-internal/models";
|
||||||
|
import { platform } from "@yaakapp-internal/platform";
|
||||||
|
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import { useCallback, useEffect, useMemo } from "react";
|
import { useCallback, useEffect, useMemo } from "react";
|
||||||
@@ -11,6 +12,7 @@ import type { DropdownItem } from "../core/Dropdown";
|
|||||||
import { Dropdown } from "../core/Dropdown";
|
import { Dropdown } from "../core/Dropdown";
|
||||||
import type { EditorProps } from "../core/Editor/Editor";
|
import type { EditorProps } from "../core/Editor/Editor";
|
||||||
import { Editor } from "../core/Editor/LazyEditor";
|
import { Editor } from "../core/Editor/LazyEditor";
|
||||||
|
import { IconButton } from "../core/IconButton";
|
||||||
import type { RadioDropdownItem } from "../core/RadioDropdown";
|
import type { RadioDropdownItem } from "../core/RadioDropdown";
|
||||||
import { RadioDropdown } from "../core/RadioDropdown";
|
import { RadioDropdown } from "../core/RadioDropdown";
|
||||||
import { Banner, FormattedError, Icon } from "@yaakapp-internal/ui";
|
import { Banner, FormattedError, Icon } from "@yaakapp-internal/ui";
|
||||||
@@ -18,6 +20,7 @@ import { Separator } from "../core/Separator";
|
|||||||
import { tryFormatGraphql } from "../../lib/formatters";
|
import { tryFormatGraphql } from "../../lib/formatters";
|
||||||
import { parseGraphQLOperationNames } from "../../lib/graphqlOperationNames";
|
import { parseGraphQLOperationNames } from "../../lib/graphqlOperationNames";
|
||||||
import { normalizeGraphQLBody } from "../../lib/requestBodyConversion";
|
import { normalizeGraphQLBody } from "../../lib/requestBodyConversion";
|
||||||
|
import { revealInFinderText } from "../../lib/reveal";
|
||||||
import { showGraphQLDocExplorerAtom } from "./graphqlAtoms";
|
import { showGraphQLDocExplorerAtom } from "./graphqlAtoms";
|
||||||
|
|
||||||
type Props = Pick<EditorProps, "heightMode" | "className" | "forceUpdateKey"> & {
|
type Props = Pick<EditorProps, "heightMode" | "className" | "forceUpdateKey"> & {
|
||||||
@@ -28,6 +31,10 @@ type Props = Pick<EditorProps, "heightMode" | "className" | "forceUpdateKey"> &
|
|||||||
|
|
||||||
const OPERATION_NAME_NOT_SPECIFIED = "";
|
const OPERATION_NAME_NOT_SPECIFIED = "";
|
||||||
|
|
||||||
|
// How much of the end of a schema filename is pinned when middle-truncating it.
|
||||||
|
// Enough to keep the extension and a little of the name before it.
|
||||||
|
const FILE_NAME_TAIL_CHARS = 12;
|
||||||
|
|
||||||
export function GraphQLEditor(props: Props) {
|
export function GraphQLEditor(props: Props) {
|
||||||
// There's some weirdness with stale onChange being called when switching requests, so we'll
|
// There's some weirdness with stale onChange being called when switching requests, so we'll
|
||||||
// key on the request ID as a workaround for now.
|
// key on the request ID as a workaround for now.
|
||||||
@@ -38,9 +45,41 @@ function GraphQLEditorInner({ request, onChange, baseRequest, ...extraEditorProp
|
|||||||
const [autoIntrospectDisabled, setAutoIntrospectDisabled] = useLocalStorage<
|
const [autoIntrospectDisabled, setAutoIntrospectDisabled] = useLocalStorage<
|
||||||
Record<string, boolean>
|
Record<string, boolean>
|
||||||
>("graphQLAutoIntrospectDisabled", {});
|
>("graphQLAutoIntrospectDisabled", {});
|
||||||
const { schema, isLoading, error, refetch, clear } = useIntrospectGraphQL(baseRequest, {
|
const {
|
||||||
|
schema,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
refetch,
|
||||||
|
clear,
|
||||||
|
loadFromFile,
|
||||||
|
reloadFromFile,
|
||||||
|
removeSchemaFile,
|
||||||
|
filePath,
|
||||||
|
} = useIntrospectGraphQL(baseRequest, {
|
||||||
disabled: autoIntrospectDisabled?.[baseRequest.id],
|
disabled: autoIntrospectDisabled?.[baseRequest.id],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Last path segment, for display only. The host owns real path semantics; this
|
||||||
|
// just needs something short enough to label the divider with.
|
||||||
|
const fileName = useMemo(() => filePath?.split(/[/\\]/).pop() || filePath, [filePath]);
|
||||||
|
|
||||||
|
// Selecting a file is all it takes — the request's source becomes that file,
|
||||||
|
// which is what keeps automatic introspection from overwriting it.
|
||||||
|
const handleLoadFromFile = useCallback(async () => {
|
||||||
|
const selected = await platform.dialog.open({
|
||||||
|
title: "Load GraphQL Schema",
|
||||||
|
multiple: false,
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
name: "GraphQL Schema",
|
||||||
|
extensions: ["graphql", "graphqls", "gql", "json"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
if (selected == null) return;
|
||||||
|
|
||||||
|
await loadFromFile(selected);
|
||||||
|
}, [loadFromFile]);
|
||||||
const [currentBody, setCurrentBody] = useStateWithDeps<{
|
const [currentBody, setCurrentBody] = useStateWithDeps<{
|
||||||
query: string;
|
query: string;
|
||||||
variables: string | undefined;
|
variables: string | undefined;
|
||||||
@@ -160,14 +199,37 @@ function GraphQLEditorInner({ request, onChange, baseRequest, ...extraEditorProp
|
|||||||
...((schema != null
|
...((schema != null
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
label: "Clear",
|
label: "Clear Schema",
|
||||||
onSelect: clear,
|
onSelect: clear,
|
||||||
color: "danger",
|
color: "danger",
|
||||||
leftSlot: <Icon icon="trash" />,
|
leftSlot: <Icon icon="trash" />,
|
||||||
},
|
},
|
||||||
{ type: "separator" },
|
|
||||||
]
|
]
|
||||||
: []) satisfies DropdownItem[]),
|
: []) satisfies DropdownItem[]),
|
||||||
|
{
|
||||||
|
// Labels the source actions below it, so the menu says where the
|
||||||
|
// schema came from without spending a row on it.
|
||||||
|
type: "separator",
|
||||||
|
hidden: schema == null && filePath == null,
|
||||||
|
label:
|
||||||
|
fileName == null || filePath == null ? undefined : (
|
||||||
|
// Middle truncation: the head shrinks and ellipsizes while the
|
||||||
|
// tail is pinned, so the extension always survives. Full path
|
||||||
|
// on hover.
|
||||||
|
<div className="flex min-w-0 max-w-[16rem] font-mono text-xs" title={filePath}>
|
||||||
|
<span className="truncate">{fileName.slice(0, -FILE_NAME_TAIL_CHARS)}</span>
|
||||||
|
<span className="shrink-0">{fileName.slice(-FILE_NAME_TAIL_CHARS)}</span>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
action:
|
||||||
|
filePath == null
|
||||||
|
? undefined
|
||||||
|
: {
|
||||||
|
icon: "folder_symlink",
|
||||||
|
title: revealInFinderText,
|
||||||
|
onClick: () => platform.revealItemInDir(filePath),
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
hidden: !error,
|
hidden: !error,
|
||||||
label: (
|
label: (
|
||||||
@@ -210,25 +272,33 @@ function GraphQLEditorInner({ request, onChange, baseRequest, ...extraEditorProp
|
|||||||
type: "content",
|
type: "content",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
hidden: schema == null,
|
// One refresh action for either source: re-read the file, or
|
||||||
label: `${isDocOpen ? "Hide" : "Show"} Documentation`,
|
// re-introspect the server.
|
||||||
leftSlot: <Icon icon="book_open_text" />,
|
label: "Reload Schema",
|
||||||
onSelect: () => {
|
|
||||||
setGraphqlDocStateAtomValue((v) => ({
|
|
||||||
...v,
|
|
||||||
[request.id]: isDocOpen ? undefined : null,
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Introspect Schema",
|
|
||||||
leftSlot: <Icon icon="refresh" spin={isLoading} />,
|
leftSlot: <Icon icon="refresh" spin={isLoading} />,
|
||||||
keepOpenOnSelect: true,
|
keepOpenOnSelect: true,
|
||||||
onSelect: refetch,
|
// Failures surface through the hook's error state either way.
|
||||||
|
onSelect: async () => {
|
||||||
|
if (filePath != null) await reloadFromFile();
|
||||||
|
else await refetch();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{ type: "separator", label: "Setting" },
|
|
||||||
{
|
{
|
||||||
label: "Automatic Introspection",
|
label: filePath == null ? "Load Schema from File…" : "Load a Different File…",
|
||||||
|
leftSlot: <Icon icon="import" />,
|
||||||
|
onSelect: handleLoadFromFile,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hidden: filePath == null,
|
||||||
|
label: "Stop Using File",
|
||||||
|
leftSlot: <Icon icon="x" />,
|
||||||
|
onSelect: removeSchemaFile,
|
||||||
|
},
|
||||||
|
{ type: "separator", label: "Settings" },
|
||||||
|
{
|
||||||
|
// Governs both sources: re-introspecting the server, and
|
||||||
|
// re-reading the file when the request is opened.
|
||||||
|
label: filePath == null ? "Automatic Introspection" : "Automatic Reload",
|
||||||
keepOpenOnSelect: true,
|
keepOpenOnSelect: true,
|
||||||
onSelect: () => {
|
onSelect: () => {
|
||||||
setAutoIntrospectDisabled({
|
setAutoIntrospectDisabled({
|
||||||
@@ -261,6 +331,29 @@ function GraphQLEditorInner({ request, onChange, baseRequest, ...extraEditorProp
|
|||||||
</Dropdown>
|
</Dropdown>
|
||||||
)}
|
)}
|
||||||
</div>,
|
</div>,
|
||||||
|
// Sits after the schema control it depends on. Always rendered, disabled
|
||||||
|
// without a schema, so the row never changes shape.
|
||||||
|
<div key="documentation" className="opacity-100!">
|
||||||
|
<IconButton
|
||||||
|
size="sm"
|
||||||
|
variant="border"
|
||||||
|
icon="book_open_text"
|
||||||
|
disabled={schema == null}
|
||||||
|
title={
|
||||||
|
schema == null
|
||||||
|
? "Documentation unavailable without a schema"
|
||||||
|
: isDocOpen
|
||||||
|
? "Hide Documentation"
|
||||||
|
: "Show Documentation"
|
||||||
|
}
|
||||||
|
onClick={() => {
|
||||||
|
setGraphqlDocStateAtomValue((v) => ({
|
||||||
|
...v,
|
||||||
|
[request.id]: isDocOpen ? undefined : null,
|
||||||
|
}));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
schema,
|
schema,
|
||||||
@@ -272,6 +365,11 @@ function GraphQLEditorInner({ request, onChange, baseRequest, ...extraEditorProp
|
|||||||
isLoading,
|
isLoading,
|
||||||
operationNames,
|
operationNames,
|
||||||
refetch,
|
refetch,
|
||||||
|
handleLoadFromFile,
|
||||||
|
reloadFromFile,
|
||||||
|
removeSchemaFile,
|
||||||
|
filePath,
|
||||||
|
fileName,
|
||||||
autoIntrospectDisabled,
|
autoIntrospectDisabled,
|
||||||
baseRequest.id,
|
baseRequest.id,
|
||||||
setGraphqlDocStateAtomValue,
|
setGraphqlDocStateAtomValue,
|
||||||
|
|||||||
@@ -1,29 +1,29 @@
|
|||||||
import { convertFileSrc } from "@tauri-apps/api/core";
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
bodyPath?: string;
|
/** A URL for the body the host already stored. */
|
||||||
|
bodyUrl?: string;
|
||||||
data?: Uint8Array;
|
data?: Uint8Array;
|
||||||
mimeType?: string;
|
mimeType?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AudioViewer({ bodyPath, data, mimeType }: Props) {
|
export function AudioViewer({ bodyUrl, data, mimeType }: Props) {
|
||||||
const [src, setSrc] = useState<string>();
|
const [src, setSrc] = useState<string>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (bodyPath) {
|
if (bodyUrl) {
|
||||||
setSrc(convertFileSrc(bodyPath));
|
setSrc(bodyUrl);
|
||||||
} else if (data) {
|
} else if (data) {
|
||||||
// The type matters here in a way it doesn't for an image: a media element goes by what
|
// The type matters here in a way it doesn't for an image: a media element goes by what
|
||||||
// the blob declares rather than sniffing it, so an Ogg labelled as MP3 won't play
|
// the blob declares rather than sniffing it, so an Ogg labelled as MP3 won't play
|
||||||
const blob = new Blob([new Uint8Array(data)], { type: mimeType ?? "audio/mpeg" });
|
const blob = new Blob([new Uint8Array(data)], { type: mimeType ?? "audio/mpeg" });
|
||||||
const url = URL.createObjectURL(blob);
|
const objectUrl = URL.createObjectURL(blob);
|
||||||
setSrc(url);
|
setSrc(objectUrl);
|
||||||
return () => URL.revokeObjectURL(url);
|
return () => URL.revokeObjectURL(objectUrl);
|
||||||
} else {
|
} else {
|
||||||
setSrc(undefined);
|
setSrc(undefined);
|
||||||
}
|
}
|
||||||
}, [bodyPath, data, mimeType]);
|
}, [bodyUrl, data, mimeType]);
|
||||||
|
|
||||||
// oxlint-disable-next-line jsx-a11y/media-has-caption
|
// oxlint-disable-next-line jsx-a11y/media-has-caption
|
||||||
return <audio className="w-full" controls src={src} />;
|
return <audio className="w-full" controls src={src} />;
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import type { ReactNode } from "react";
|
||||||
|
import { renderToStaticMarkup } from "react-dom/server";
|
||||||
|
import { describe, expect, test, vi } from "vite-plus/test";
|
||||||
|
import { CsvViewerInner } from "./CsvViewer";
|
||||||
|
|
||||||
|
vi.mock("@yaakapp-internal/ui", () => ({
|
||||||
|
Table: ({ children }: { children: ReactNode }) => <table>{children}</table>,
|
||||||
|
TableBody: ({ children }: { children: ReactNode }) => <tbody>{children}</tbody>,
|
||||||
|
TableCell: ({ children }: { children: ReactNode }) => <td>{children}</td>,
|
||||||
|
TableHead: ({ children }: { children: ReactNode }) => <thead>{children}</thead>,
|
||||||
|
TableHeaderCell: ({ children }: { children: ReactNode }) => <th>{children}</th>,
|
||||||
|
TableRow: ({ children }: { children: ReactNode }) => <tr>{children}</tr>,
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("CsvViewer", () => {
|
||||||
|
test("renders columns that extend beyond the first row", () => {
|
||||||
|
const markup = renderToStaticMarkup(
|
||||||
|
<CsvViewerInner
|
||||||
|
text={[
|
||||||
|
"startDate,2026-02-03T00:00-03:00",
|
||||||
|
"endDate,2026-02-03T23:59:59-03:00",
|
||||||
|
"id,Fecha de inicio,Nombre,Estado,Perfil de puesto,ID de sucursal,Sucursal,Fecha de fin,ID de usuario",
|
||||||
|
"391118210,2026-02-03 12:58:55,atencion1,Disponible,ATD,3549,sucursal,2026-02-03 12:59:08,42041",
|
||||||
|
].join("\n")}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(markup).toContain("ID de usuario");
|
||||||
|
expect(markup).toContain("42041");
|
||||||
|
expect(markup.match(/<td>/g)).toHaveLength(20);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -26,27 +26,33 @@ export function CsvViewer({ text, className }: Props) {
|
|||||||
export function CsvViewerInner({ text, className }: { text: string | null; className?: string }) {
|
export function CsvViewerInner({ text, className }: { text: string | null; className?: string }) {
|
||||||
const parsed = useMemo(() => {
|
const parsed = useMemo(() => {
|
||||||
if (text == null) return null;
|
if (text == null) return null;
|
||||||
return Papa.parse<Record<string, string>>(text, { header: true, skipEmptyLines: true });
|
return Papa.parse<string[]>(text, { skipEmptyLines: true });
|
||||||
}, [text]);
|
}, [text]);
|
||||||
|
|
||||||
if (parsed === null) return null;
|
if (parsed === null) return null;
|
||||||
|
|
||||||
|
const header = parsed.data[0] ?? [];
|
||||||
|
const rows = parsed.data.slice(1);
|
||||||
|
const columnCount = parsed.data.reduce((count, row) => Math.max(count, row.length), 0);
|
||||||
|
const columnIndexes = Array.from({ length: columnCount }, (_, index) => index);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="overflow-auto h-full">
|
<div className="overflow-auto h-full">
|
||||||
<Table className={classNames(className, "text-sm")}>
|
<Table className={classNames(className, "text-sm")}>
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
{parsed.meta.fields?.map((field) => (
|
{columnIndexes.map((columnIndex) => (
|
||||||
<TableHeaderCell key={field}>{field}</TableHeaderCell>
|
<TableHeaderCell key={columnIndex}>{header[columnIndex] ?? ""}</TableHeaderCell>
|
||||||
))}
|
))}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{parsed.data.map((row, i) => (
|
{rows.map((row, i) => (
|
||||||
// oxlint-disable-next-line react/no-array-index-key
|
// oxlint-disable-next-line react/no-array-index-key
|
||||||
<TableRow key={i}>
|
<TableRow key={i}>
|
||||||
{parsed.meta.fields?.map((key) => (
|
{row.map((cell, columnIndex) => (
|
||||||
<TableCell key={key}>{row[key] ?? ""}</TableCell>
|
// oxlint-disable-next-line react/no-array-index-key
|
||||||
|
<TableCell key={columnIndex}>{cell}</TableCell>
|
||||||
))}
|
))}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import type { HttpResponse } from "@yaakapp-internal/models";
|
import type { HttpResponse } from "@yaakapp-internal/models";
|
||||||
import { useMemo, useState } from "react";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { useCallback } from "react";
|
||||||
import { useCopyHttpResponse } from "../../hooks/useCopyHttpResponse";
|
import { useCopyHttpResponse } from "../../hooks/useCopyHttpResponse";
|
||||||
import { useResponseBodyText } from "../../hooks/useResponseBodyText";
|
import { responseBodyTextQuery, useResponseBodyText } from "../../hooks/useResponseBodyText";
|
||||||
|
import { useResponseFilter } from "../../hooks/useResponseFilter";
|
||||||
import { useSaveResponse } from "../../hooks/useSaveResponse";
|
import { useSaveResponse } from "../../hooks/useSaveResponse";
|
||||||
import { languageFromContentType } from "../../lib/contentType";
|
import { languageFromContentType } from "../../lib/contentType";
|
||||||
import { getContentTypeFromHeaders } from "../../lib/model_util";
|
import { getContentTypeFromHeaders } from "../../lib/model_util";
|
||||||
@@ -52,30 +54,25 @@ interface HttpTextViewerProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function HttpTextViewer({ response, text, language, pretty, className }: HttpTextViewerProps) {
|
function HttpTextViewer({ response, text, language, pretty, className }: HttpTextViewerProps) {
|
||||||
const [currentFilter, setCurrentFilter] = useState<string | null>(null);
|
const queryClient = useQueryClient();
|
||||||
const filteredBody = useResponseBodyText({ response, filter: currentFilter });
|
const filter = useResponseFilter({
|
||||||
|
stateKey: `response.body.${response.requestId}`,
|
||||||
|
// Shares the display query's cache entry, so the verdict costs no extra RPC
|
||||||
|
runFilter: useCallback(
|
||||||
|
(f: string) => queryClient.fetchQuery(responseBodyTextQuery({ response, filter: f })),
|
||||||
|
[queryClient, response],
|
||||||
|
),
|
||||||
|
});
|
||||||
|
const filteredBody = useResponseBodyText({ response, filter: filter.appliedFilter });
|
||||||
const saveResponse = useSaveResponse(response);
|
const saveResponse = useSaveResponse(response);
|
||||||
const copyResponse = useCopyHttpResponse(response);
|
const copyResponse = useCopyHttpResponse(response);
|
||||||
const actionsDisabled = response.state !== "closed" && response.status >= 100;
|
const actionsDisabled = response.state !== "closed" && response.status >= 100;
|
||||||
|
|
||||||
const filterCallback = useMemo(
|
|
||||||
() => (filter: string) => {
|
|
||||||
setCurrentFilter(filter);
|
|
||||||
return {
|
|
||||||
data: filteredBody.data,
|
|
||||||
isPending: filteredBody.isPending,
|
|
||||||
error: !!filteredBody.error,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
[filteredBody],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TextViewer
|
<TextViewer
|
||||||
text={text}
|
text={text}
|
||||||
language={language}
|
language={language}
|
||||||
stateKey={`response.body.${response.id}`}
|
stateKey={`response.body.${response.id}`}
|
||||||
filterStateKey={`response.body.${response.requestId}`}
|
|
||||||
pretty={pretty}
|
pretty={pretty}
|
||||||
className={className}
|
className={className}
|
||||||
footerActions={[
|
footerActions={[
|
||||||
@@ -98,7 +95,12 @@ function HttpTextViewer({ response, text, language, pretty, className }: HttpTex
|
|||||||
className="border !border-border-subtle"
|
className="border !border-border-subtle"
|
||||||
/>,
|
/>,
|
||||||
]}
|
]}
|
||||||
onFilter={filterCallback}
|
filter={filter}
|
||||||
|
filterResult={{
|
||||||
|
data: filteredBody.data,
|
||||||
|
isPending: filteredBody.isPending,
|
||||||
|
error: !!filteredBody.error,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { convertFileSrc } from "@tauri-apps/api/core";
|
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
type Props = { className?: string; mimeType?: string } & (
|
type Props = { className?: string; mimeType?: string } & (
|
||||||
| {
|
| {
|
||||||
bodyPath: string;
|
/** A URL for the body the host already stored. */
|
||||||
|
bodyUrl: string;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
data: ArrayBuffer;
|
data: ArrayBuffer;
|
||||||
@@ -13,21 +13,21 @@ type Props = { className?: string; mimeType?: string } & (
|
|||||||
|
|
||||||
export function ImageViewer({ className, mimeType, ...props }: Props) {
|
export function ImageViewer({ className, mimeType, ...props }: Props) {
|
||||||
const [src, setSrc] = useState<string>();
|
const [src, setSrc] = useState<string>();
|
||||||
const bodyPath = "bodyPath" in props ? props.bodyPath : null;
|
const bodyUrl = "bodyUrl" in props ? props.bodyUrl : null;
|
||||||
const data = "data" in props ? props.data : null;
|
const data = "data" in props ? props.data : null;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (bodyPath != null) {
|
if (bodyUrl != null) {
|
||||||
setSrc(convertFileSrc(bodyPath));
|
setSrc(bodyUrl);
|
||||||
} else if (data != null) {
|
} else if (data != null) {
|
||||||
const blob = new Blob([data], { type: mimeType ?? "image/png" });
|
const blob = new Blob([data], { type: mimeType ?? "image/png" });
|
||||||
const url = URL.createObjectURL(blob);
|
const objectUrl = URL.createObjectURL(blob);
|
||||||
setSrc(url);
|
setSrc(objectUrl);
|
||||||
return () => URL.revokeObjectURL(url);
|
return () => URL.revokeObjectURL(objectUrl);
|
||||||
} else {
|
} else {
|
||||||
setSrc(undefined);
|
setSrc(undefined);
|
||||||
}
|
}
|
||||||
}, [bodyPath, data, mimeType]);
|
}, [bodyUrl, data, mimeType]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<img
|
<img
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import "react-pdf/dist/Page/TextLayer.css";
|
import "react-pdf/dist/Page/TextLayer.css";
|
||||||
import "react-pdf/dist/Page/AnnotationLayer.css";
|
import "react-pdf/dist/Page/AnnotationLayer.css";
|
||||||
import { convertFileSrc } from "@tauri-apps/api/core";
|
|
||||||
import "./PdfViewer.css";
|
import "./PdfViewer.css";
|
||||||
import type { PDFDocumentProxy } from "pdfjs-dist";
|
import type { PDFDocumentProxy } from "pdfjs-dist";
|
||||||
import { useMemo, useRef, useState } from "react";
|
import { useMemo, useRef, useState } from "react";
|
||||||
@@ -18,7 +17,8 @@ fireAndForget(
|
|||||||
);
|
);
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
bodyPath?: string;
|
/** A URL for the body the host already stored. */
|
||||||
|
bodyUrl?: string;
|
||||||
data?: Uint8Array;
|
data?: Uint8Array;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ const options = {
|
|||||||
standardFontDataUrl: "/standard_fonts/",
|
standardFontDataUrl: "/standard_fonts/",
|
||||||
};
|
};
|
||||||
|
|
||||||
export function PdfViewer({ bodyPath, data }: Props) {
|
export function PdfViewer({ bodyUrl, data }: Props) {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const [numPages, setNumPages] = useState<number>();
|
const [numPages, setNumPages] = useState<number>();
|
||||||
|
|
||||||
@@ -36,8 +36,8 @@ export function PdfViewer({ bodyPath, data }: Props) {
|
|||||||
// During render, not in an effect: an effect leaves the first paint with no file, and
|
// During render, not in an effect: an effect leaves the first paint with no file, and
|
||||||
// `Document` renders its "Failed to load PDF file" state for that frame before recovering
|
// `Document` renders its "Failed to load PDF file" state for that frame before recovering
|
||||||
const src = useMemo(() => {
|
const src = useMemo(() => {
|
||||||
if (bodyPath) {
|
if (bodyUrl) {
|
||||||
return convertFileSrc(bodyPath);
|
return bodyUrl;
|
||||||
}
|
}
|
||||||
if (data) {
|
if (data) {
|
||||||
// Create a copy to avoid "Buffer is already detached" errors
|
// Create a copy to avoid "Buffer is already detached" errors
|
||||||
@@ -45,7 +45,7 @@ export function PdfViewer({ bodyPath, data }: Props) {
|
|||||||
return { data: new Uint8Array(data) };
|
return { data: new Uint8Array(data) };
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}, [bodyPath, data]);
|
}, [bodyUrl, data]);
|
||||||
|
|
||||||
const onDocumentLoadSuccess = ({ numPages: nextNumPages }: PDFDocumentProxy): void => {
|
const onDocumentLoadSuccess = ({ numPages: nextNumPages }: PDFDocumentProxy): void => {
|
||||||
setNumPages(nextNumPages);
|
setNumPages(nextNumPages);
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import { Icon } from "@yaakapp-internal/ui";
|
||||||
|
import type { RecentFilter } from "../../hooks/useRecentFilters";
|
||||||
|
import { Dropdown, type DropdownItem } from "../core/Dropdown";
|
||||||
|
import { IconButton } from "../core/IconButton";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
recentFilters: RecentFilter[];
|
||||||
|
activeFilter: string | null;
|
||||||
|
onSelect: (value: string) => void;
|
||||||
|
onRemove: (value: string) => void;
|
||||||
|
onTogglePin: (value: string) => void;
|
||||||
|
onClear: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RecentFiltersDropdown({
|
||||||
|
recentFilters,
|
||||||
|
activeFilter,
|
||||||
|
onSelect,
|
||||||
|
onRemove,
|
||||||
|
onTogglePin,
|
||||||
|
onClear,
|
||||||
|
}: Props) {
|
||||||
|
const pinned = recentFilters.filter((f) => f.pinned);
|
||||||
|
const unpinned = recentFilters.filter((f) => !f.pinned);
|
||||||
|
|
||||||
|
const toItem = (filter: RecentFilter): DropdownItem => ({
|
||||||
|
label: (
|
||||||
|
<div className="font-mono text-sm truncate max-w-sm" title={filter.value}>
|
||||||
|
{filter.value}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
leftSlot: <Icon icon={filter.value === activeFilter ? "check" : "empty"} />,
|
||||||
|
onSelect: () => onSelect(filter.value),
|
||||||
|
submenuTrigger: "button",
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
label: filter.pinned ? "Unpin" : "Pin",
|
||||||
|
icon: filter.pinned ? "unpin" : "pin",
|
||||||
|
keepOpenOnSelect: true,
|
||||||
|
onSelect: () => onTogglePin(filter.value),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Remove",
|
||||||
|
icon: "trash",
|
||||||
|
color: "danger",
|
||||||
|
keepOpenOnSelect: true,
|
||||||
|
onSelect: () => onRemove(filter.value),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const items: DropdownItem[] = [];
|
||||||
|
|
||||||
|
if (recentFilters.length === 0) {
|
||||||
|
items.push({
|
||||||
|
type: "content",
|
||||||
|
label: (
|
||||||
|
<span className="block px-4 py-1 text-sm text-text-subtle">
|
||||||
|
Filters you use are remembered here
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pinned.length > 0) {
|
||||||
|
items.push({ type: "separator", label: "Pinned" }, ...pinned.map(toItem));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unpinned.length > 0) {
|
||||||
|
items.push({ type: "separator", label: "Recent" }, ...unpinned.map(toItem));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recentFilters.length > 0) {
|
||||||
|
items.push(
|
||||||
|
{ type: "separator" },
|
||||||
|
{
|
||||||
|
label: "Clear All",
|
||||||
|
leftSlot: <Icon icon="trash" />,
|
||||||
|
color: "danger",
|
||||||
|
onSelect: onClear,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dropdown items={items}>
|
||||||
|
<IconButton
|
||||||
|
size="xs"
|
||||||
|
icon="filter"
|
||||||
|
title="Recent filters"
|
||||||
|
iconColor="secondary"
|
||||||
|
className="w-8 ml-0.5 mr-1 h-auto!"
|
||||||
|
/>
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,14 +1,15 @@
|
|||||||
import classNames from "classnames";
|
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { Children, useCallback, useMemo } from "react";
|
import { Children, useCallback, useMemo } from "react";
|
||||||
import { createGlobalState } from "react-use";
|
import { Banner, HStack, Icon, InlineCode } from "@yaakapp-internal/ui";
|
||||||
import { useDebouncedValue } from "@yaakapp-internal/ui";
|
|
||||||
import { useFormatText } from "../../hooks/useFormatText";
|
import { useFormatText } from "../../hooks/useFormatText";
|
||||||
|
import type { ResponseFilterApi } from "../../hooks/useResponseFilter";
|
||||||
|
import { Button } from "../core/Button";
|
||||||
import type { EditorProps } from "../core/Editor/Editor";
|
import type { EditorProps } from "../core/Editor/Editor";
|
||||||
import { hyperlink } from "../core/Editor/hyperlink/extension";
|
import { hyperlink } from "../core/Editor/hyperlink/extension";
|
||||||
import { Editor } from "../core/Editor/LazyEditor";
|
import { Editor } from "../core/Editor/LazyEditor";
|
||||||
import { IconButton } from "../core/IconButton";
|
import { IconButton } from "../core/IconButton";
|
||||||
import { Input } from "../core/Input";
|
import { Input } from "../core/Input";
|
||||||
|
import { RecentFiltersDropdown } from "./RecentFiltersDropdown";
|
||||||
|
|
||||||
const extraExtensions = [hyperlink];
|
const extraExtensions = [hyperlink];
|
||||||
|
|
||||||
@@ -16,56 +17,44 @@ interface Props {
|
|||||||
text: string;
|
text: string;
|
||||||
language: EditorProps["language"];
|
language: EditorProps["language"];
|
||||||
stateKey: string | null;
|
stateKey: string | null;
|
||||||
filterStateKey?: string | null;
|
|
||||||
pretty?: boolean;
|
pretty?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
footerActions?: ReactNode;
|
footerActions?: ReactNode;
|
||||||
onFilter?: (filter: string) => {
|
filter?: ResponseFilterApi;
|
||||||
|
filterResult?: {
|
||||||
data: string | null | undefined;
|
data: string | null | undefined;
|
||||||
isPending: boolean;
|
isPending: boolean;
|
||||||
error: boolean;
|
error: boolean;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const useFilterText = createGlobalState<Record<string, string | null>>({});
|
|
||||||
|
|
||||||
export function TextViewer({
|
export function TextViewer({
|
||||||
language,
|
language,
|
||||||
text,
|
text,
|
||||||
stateKey,
|
stateKey,
|
||||||
filterStateKey,
|
|
||||||
pretty,
|
pretty,
|
||||||
className,
|
className,
|
||||||
footerActions,
|
footerActions,
|
||||||
onFilter,
|
filter,
|
||||||
|
filterResult,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const filterKey = filterStateKey ?? stateKey;
|
const canFilter =
|
||||||
const [filterTextMap, setFilterTextMap] = useFilterText();
|
filter != null && (language === "json" || language === "xml" || language === "html");
|
||||||
const filterText = filterKey ? (filterTextMap[filterKey] ?? null) : null;
|
const isSearching = filter?.isSearching ?? false;
|
||||||
const debouncedFilterText = useDebouncedValue(filterText);
|
const appliedFilter = filter?.appliedFilter ?? null;
|
||||||
const setFilterText = useCallback(
|
const resultError = filterResult?.error ?? false;
|
||||||
(v: string | null) => {
|
|
||||||
if (!filterKey) return;
|
|
||||||
setFilterTextMap((m) => ({ ...m, [filterKey]: v }));
|
|
||||||
},
|
|
||||||
[filterKey, setFilterTextMap],
|
|
||||||
);
|
|
||||||
|
|
||||||
const isSearching = filterText != null;
|
const handleFilterKeyDown = useCallback(
|
||||||
const filteredResponse =
|
(e: KeyboardEvent) => {
|
||||||
onFilter && debouncedFilterText
|
if (filter == null) return;
|
||||||
? onFilter(debouncedFilterText)
|
if (e.key === "Escape") {
|
||||||
: { data: null, isPending: false, error: false };
|
filter.toggleSearch();
|
||||||
|
} else if (e.key === "Enter" && filter.filterText != null) {
|
||||||
const toggleSearch = useCallback(() => {
|
filter.applyFilter(filter.filterText);
|
||||||
if (isSearching) {
|
|
||||||
setFilterText(null);
|
|
||||||
} else {
|
|
||||||
setFilterText("");
|
|
||||||
}
|
}
|
||||||
}, [isSearching, setFilterText]);
|
},
|
||||||
|
[filter],
|
||||||
const canFilter = onFilter && (language === "json" || language === "xml" || language === "html");
|
);
|
||||||
|
|
||||||
const actions = useMemo<ReactNode[]>(() => {
|
const actions = useMemo<ReactNode[]>(() => {
|
||||||
const nodes: ReactNode[] = isSearching ? [] : Children.toArray(footerActions);
|
const nodes: ReactNode[] = isSearching ? [] : Children.toArray(footerActions);
|
||||||
@@ -76,8 +65,8 @@ export function TextViewer({
|
|||||||
nodes.push(
|
nodes.push(
|
||||||
<div key="input" className="w-full opacity-100!">
|
<div key="input" className="w-full opacity-100!">
|
||||||
<Input
|
<Input
|
||||||
key={filterKey ?? "filter"}
|
key={filter.stateKey ?? "filter"}
|
||||||
validate={!filteredResponse.error}
|
validate={!resultError}
|
||||||
hideLabel
|
hideLabel
|
||||||
autoFocus
|
autoFocus
|
||||||
containerClassName="bg-surface"
|
containerClassName="bg-surface"
|
||||||
@@ -85,39 +74,62 @@ export function TextViewer({
|
|||||||
placeholder={language === "json" ? "JSONPath expression" : "XPath expression"}
|
placeholder={language === "json" ? "JSONPath expression" : "XPath expression"}
|
||||||
label="Filter expression"
|
label="Filter expression"
|
||||||
name="filter"
|
name="filter"
|
||||||
defaultValue={filterText}
|
defaultValue={filter.filterText}
|
||||||
onKeyDown={(e) => e.key === "Escape" && toggleSearch()}
|
forceUpdateKey={filter.filterUpdateKey}
|
||||||
onChange={setFilterText}
|
onKeyDown={handleFilterKeyDown}
|
||||||
stateKey={filterKey ? `filter.${filterKey}` : null}
|
onChange={filter.setFilterText}
|
||||||
|
stateKey={filter.stateKey ? `filter.${filter.stateKey}` : null}
|
||||||
|
leftSlot={
|
||||||
|
<div className="py-0.5 flex">
|
||||||
|
<RecentFiltersDropdown
|
||||||
|
recentFilters={filter.recentFilters}
|
||||||
|
activeFilter={filter.appliedFilter}
|
||||||
|
onSelect={filter.replaceFilter}
|
||||||
|
onRemove={filter.removeRecentFilter}
|
||||||
|
onTogglePin={filter.togglePinRecentFilter}
|
||||||
|
onClear={filter.clearRecentFilters}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
rightSlot={
|
||||||
|
<div className="py-0.5 flex">
|
||||||
|
<IconButton
|
||||||
|
size="xs"
|
||||||
|
icon="x"
|
||||||
|
title="Close filter"
|
||||||
|
iconColor="secondary"
|
||||||
|
onClick={filter.toggleSearch}
|
||||||
|
className="w-8 mr-0.5 h-auto!"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>,
|
</div>,
|
||||||
);
|
);
|
||||||
}
|
} else {
|
||||||
|
|
||||||
nodes.push(
|
nodes.push(
|
||||||
<IconButton
|
<IconButton
|
||||||
key="icon"
|
key="icon"
|
||||||
size="sm"
|
size="sm"
|
||||||
isLoading={filteredResponse.isPending}
|
isLoading={filterResult?.isPending ?? false}
|
||||||
icon={isSearching ? "x" : "filter"}
|
icon="filter"
|
||||||
title={isSearching ? "Close filter" : "Filter response"}
|
title="Filter response"
|
||||||
onClick={toggleSearch}
|
onClick={filter.toggleSearch}
|
||||||
className={classNames("border border-border-subtle!", isSearching && "opacity-100!")}
|
className="border border-border-subtle!"
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return nodes;
|
return nodes;
|
||||||
}, [
|
}, [
|
||||||
canFilter,
|
canFilter,
|
||||||
footerActions,
|
footerActions,
|
||||||
filterKey,
|
filter,
|
||||||
filterText,
|
filterResult?.isPending,
|
||||||
filteredResponse.error,
|
resultError,
|
||||||
filteredResponse.isPending,
|
|
||||||
isSearching,
|
isSearching,
|
||||||
language,
|
language,
|
||||||
setFilterText,
|
handleFilterKeyDown,
|
||||||
toggleSearch,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const formattedBody = useFormatText({ text, language, pretty: pretty ?? false });
|
const formattedBody = useFormatText({ text, language, pretty: pretty ?? false });
|
||||||
@@ -126,11 +138,11 @@ export function TextViewer({
|
|||||||
}
|
}
|
||||||
|
|
||||||
let body: string;
|
let body: string;
|
||||||
if (isSearching && filterText?.length > 0) {
|
if (appliedFilter) {
|
||||||
if (filteredResponse.error) {
|
if (resultError) {
|
||||||
body = "";
|
body = "";
|
||||||
} else {
|
} else {
|
||||||
body = filteredResponse.data != null ? filteredResponse.data : "";
|
body = filterResult?.data != null ? filterResult.data : "";
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
body = formattedBody;
|
body = formattedBody;
|
||||||
@@ -143,6 +155,16 @@ export function TextViewer({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div className="grid grid-rows-[auto_minmax(0,1fr)] h-full w-full">
|
||||||
|
{appliedFilter && filter != null ? (
|
||||||
|
<AppliedFilterBar
|
||||||
|
filter={appliedFilter}
|
||||||
|
error={resultError}
|
||||||
|
onClear={() => filter.replaceFilter("")}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span />
|
||||||
|
)}
|
||||||
<Editor
|
<Editor
|
||||||
readOnly
|
readOnly
|
||||||
className={className}
|
className={className}
|
||||||
@@ -152,6 +174,42 @@ export function TextViewer({
|
|||||||
extraExtensions={extraExtensions}
|
extraExtensions={extraExtensions}
|
||||||
stateKey={stateKey}
|
stateKey={stateKey}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows what's actually filtering the body, which the filter box below can't convey
|
||||||
|
* once it holds an edited expression that hasn't been applied yet.
|
||||||
|
*/
|
||||||
|
function AppliedFilterBar({
|
||||||
|
filter,
|
||||||
|
error,
|
||||||
|
onClear,
|
||||||
|
}: {
|
||||||
|
filter: string;
|
||||||
|
error: boolean;
|
||||||
|
onClear: () => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Banner color={error ? "danger" : "info"} className="py-1! mb-2! text-sm">
|
||||||
|
<HStack space={2} className="min-w-0">
|
||||||
|
<Icon icon="filter" size="xs" className="shrink-0 opacity-70" />
|
||||||
|
<span className="truncate min-w-0" title={filter}>
|
||||||
|
Response filtered by <InlineCode>{filter}</InlineCode>
|
||||||
|
{error && " (invalid expression)"}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
size="2xs"
|
||||||
|
variant="border"
|
||||||
|
color={error ? "danger" : "info"}
|
||||||
|
className="ml-auto shrink-0"
|
||||||
|
onClick={onClear}
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</Button>
|
||||||
|
</HStack>
|
||||||
|
</Banner>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,28 +1,28 @@
|
|||||||
import { convertFileSrc } from "@tauri-apps/api/core";
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
bodyPath?: string;
|
/** A URL for the body the host already stored. */
|
||||||
|
bodyUrl?: string;
|
||||||
data?: Uint8Array;
|
data?: Uint8Array;
|
||||||
mimeType?: string;
|
mimeType?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function VideoViewer({ bodyPath, data, mimeType }: Props) {
|
export function VideoViewer({ bodyUrl, data, mimeType }: Props) {
|
||||||
const [src, setSrc] = useState<string>();
|
const [src, setSrc] = useState<string>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (bodyPath) {
|
if (bodyUrl) {
|
||||||
setSrc(convertFileSrc(bodyPath));
|
setSrc(bodyUrl);
|
||||||
} else if (data) {
|
} else if (data) {
|
||||||
// As in AudioViewer: a media element trusts the declared type instead of sniffing
|
// As in AudioViewer: a media element trusts the declared type instead of sniffing
|
||||||
const blob = new Blob([new Uint8Array(data)], { type: mimeType ?? "video/mp4" });
|
const blob = new Blob([new Uint8Array(data)], { type: mimeType ?? "video/mp4" });
|
||||||
const url = URL.createObjectURL(blob);
|
const objectUrl = URL.createObjectURL(blob);
|
||||||
setSrc(url);
|
setSrc(objectUrl);
|
||||||
return () => URL.revokeObjectURL(url);
|
return () => URL.revokeObjectURL(objectUrl);
|
||||||
} else {
|
} else {
|
||||||
setSrc(undefined);
|
setSrc(undefined);
|
||||||
}
|
}
|
||||||
}, [bodyPath, data, mimeType]);
|
}, [bodyUrl, data, mimeType]);
|
||||||
|
|
||||||
// oxlint-disable-next-line jsx-a11y/media-has-caption
|
// oxlint-disable-next-line jsx-a11y/media-has-caption
|
||||||
return <video className="w-full" controls src={src} />;
|
return <video className="w-full" controls src={src} />;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Listen for settings changes, the re-compute theme
|
// Listen for settings changes, the re-compute theme
|
||||||
import { listen } from "@tauri-apps/api/event";
|
import { platform } from "@yaakapp-internal/platform";
|
||||||
import type { ModelPayload } from "@yaakapp-internal/models";
|
import type { ModelPayload } from "@yaakapp-internal/models";
|
||||||
import { fireAndForget } from "./lib/fireAndForget";
|
import { fireAndForget } from "./lib/fireAndForget";
|
||||||
import { getSettings } from "./lib/settings";
|
import { getSettings } from "./lib/settings";
|
||||||
@@ -8,12 +8,12 @@ function setFontSizeOnDocument(fontSize: number) {
|
|||||||
document.documentElement.style.fontSize = `${fontSize}px`;
|
document.documentElement.style.fontSize = `${fontSize}px`;
|
||||||
}
|
}
|
||||||
|
|
||||||
listen<ModelPayload[]>("model_writes", async (event) => {
|
platform.listen<ModelPayload[]>("model_writes", (payloads) => {
|
||||||
for (const payload of event.payload) {
|
for (const payload of payloads) {
|
||||||
if (payload.change.type !== "upsert") continue;
|
if (payload.change.type !== "upsert") continue;
|
||||||
if (payload.model.model !== "settings") continue;
|
if (payload.model.model !== "settings") continue;
|
||||||
setFontSizeOnDocument(payload.model.interfaceFontSize);
|
setFontSizeOnDocument(payload.model.interfaceFontSize);
|
||||||
}
|
}
|
||||||
}).catch(console.error);
|
});
|
||||||
|
|
||||||
fireAndForget(getSettings().then((settings) => setFontSizeOnDocument(settings.interfaceFontSize)));
|
fireAndForget(getSettings().then((settings) => setFontSizeOnDocument(settings.interfaceFontSize)));
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Listen for settings changes, the re-compute theme
|
// Listen for settings changes, the re-compute theme
|
||||||
import { listen } from "@tauri-apps/api/event";
|
import { platform } from "@yaakapp-internal/platform";
|
||||||
import type { ModelPayload, Settings } from "@yaakapp-internal/models";
|
import type { ModelPayload, Settings } from "@yaakapp-internal/models";
|
||||||
import { fireAndForget } from "./lib/fireAndForget";
|
import { fireAndForget } from "./lib/fireAndForget";
|
||||||
import { getSettings } from "./lib/settings";
|
import { getSettings } from "./lib/settings";
|
||||||
@@ -12,12 +12,12 @@ function setFonts(settings: Settings) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
listen<ModelPayload[]>("model_writes", async (event) => {
|
platform.listen<ModelPayload[]>("model_writes", (payloads) => {
|
||||||
for (const payload of event.payload) {
|
for (const payload of payloads) {
|
||||||
if (payload.change.type !== "upsert") continue;
|
if (payload.change.type !== "upsert") continue;
|
||||||
if (payload.model.model !== "settings") continue;
|
if (payload.model.model !== "settings") continue;
|
||||||
setFonts(payload.model);
|
setFonts(payload.model);
|
||||||
}
|
}
|
||||||
}).catch(console.error);
|
});
|
||||||
|
|
||||||
fireAndForget(getSettings().then((settings) => setFonts(settings)));
|
fireAndForget(getSettings().then((settings) => setFonts(settings)));
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { event } from "@tauri-apps/api";
|
import { platform } from "@yaakapp-internal/platform";
|
||||||
import { useFastMutation } from "./useFastMutation";
|
import { useFastMutation } from "./useFastMutation";
|
||||||
|
|
||||||
export function useCancelHttpResponse(id: string | null) {
|
export function useCancelHttpResponse(id: string | null) {
|
||||||
return useFastMutation<void>({
|
return useFastMutation<void>({
|
||||||
mutationKey: ["cancel_http_response", id],
|
mutationKey: ["cancel_http_response", id],
|
||||||
mutationFn: () => event.emit(`cancel_http_response_${id}`),
|
mutationFn: () => platform.emit(`cancel_http_response_${id}`),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,13 +3,13 @@ import { InlineCode } from "@yaakapp-internal/ui";
|
|||||||
import { showAlert } from "../lib/alert";
|
import { showAlert } from "../lib/alert";
|
||||||
import { appInfo } from "../lib/appInfo";
|
import { appInfo } from "../lib/appInfo";
|
||||||
import { minPromiseMillis } from "../lib/minPromiseMillis";
|
import { minPromiseMillis } from "../lib/minPromiseMillis";
|
||||||
import { invokeCmd } from "../lib/tauri";
|
import { rpc } from "../lib/rpc";
|
||||||
|
|
||||||
export function useCheckForUpdates() {
|
export function useCheckForUpdates() {
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationKey: ["check_for_updates"],
|
mutationKey: ["check_for_updates"],
|
||||||
mutationFn: async () => {
|
mutationFn: async () => {
|
||||||
const hasUpdate: boolean = await minPromiseMillis(invokeCmd("cmd_check_for_updates"), 500);
|
const hasUpdate: boolean = await minPromiseMillis(rpc("cmd_check_for_updates"), 500);
|
||||||
if (!hasUpdate) {
|
if (!hasUpdate) {
|
||||||
showAlert({
|
showAlert({
|
||||||
id: "no-updates",
|
id: "no-updates",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { invokeCmd } from "../lib/tauri";
|
import { rpc } from "../lib/rpc";
|
||||||
import { useFastMutation } from "./useFastMutation";
|
import { useFastMutation } from "./useFastMutation";
|
||||||
|
|
||||||
export function useDeleteGrpcConnections(requestId?: string) {
|
export function useDeleteGrpcConnections(requestId?: string) {
|
||||||
@@ -6,7 +6,7 @@ export function useDeleteGrpcConnections(requestId?: string) {
|
|||||||
mutationKey: ["delete_grpc_connections", requestId],
|
mutationKey: ["delete_grpc_connections", requestId],
|
||||||
mutationFn: async () => {
|
mutationFn: async () => {
|
||||||
if (requestId === undefined) return;
|
if (requestId === undefined) return;
|
||||||
await invokeCmd("cmd_delete_all_grpc_connections", { requestId });
|
await rpc("cmd_delete_all_grpc_connections", { requestId });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { invokeCmd } from "../lib/tauri";
|
import { rpc } from "../lib/rpc";
|
||||||
import { useFastMutation } from "./useFastMutation";
|
import { useFastMutation } from "./useFastMutation";
|
||||||
|
|
||||||
export function useDeleteHttpResponses(requestId?: string) {
|
export function useDeleteHttpResponses(requestId?: string) {
|
||||||
@@ -6,7 +6,7 @@ export function useDeleteHttpResponses(requestId?: string) {
|
|||||||
mutationKey: ["delete_http_responses", requestId],
|
mutationKey: ["delete_http_responses", requestId],
|
||||||
mutationFn: async () => {
|
mutationFn: async () => {
|
||||||
if (requestId === undefined) return;
|
if (requestId === undefined) return;
|
||||||
await invokeCmd("cmd_delete_all_http_responses", { requestId });
|
await rpc("cmd_delete_all_http_responses", { requestId });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { showAlert } from "../lib/alert";
|
|||||||
import { showConfirmDelete } from "../lib/confirm";
|
import { showConfirmDelete } from "../lib/confirm";
|
||||||
import { jotaiStore } from "../lib/jotai";
|
import { jotaiStore } from "../lib/jotai";
|
||||||
import { pluralizeCount } from "../lib/pluralize";
|
import { pluralizeCount } from "../lib/pluralize";
|
||||||
import { invokeCmd } from "../lib/tauri";
|
import { rpc } from "../lib/rpc";
|
||||||
import { activeWorkspaceIdAtom } from "./useActiveWorkspace";
|
import { activeWorkspaceIdAtom } from "./useActiveWorkspace";
|
||||||
import { useFastMutation } from "./useFastMutation";
|
import { useFastMutation } from "./useFastMutation";
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ export function useDeleteSendHistory() {
|
|||||||
if (!confirmed) return false;
|
if (!confirmed) return false;
|
||||||
|
|
||||||
const workspaceId = jotaiStore.get(activeWorkspaceIdAtom);
|
const workspaceId = jotaiStore.get(activeWorkspaceIdAtom);
|
||||||
await invokeCmd("cmd_delete_send_history", { workspaceId });
|
await rpc("cmd_delete_send_history", { workspaceId });
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import type {
|
|||||||
GetFolderActionsResponse,
|
GetFolderActionsResponse,
|
||||||
} from "@yaakapp-internal/plugins";
|
} from "@yaakapp-internal/plugins";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { invokeCmd } from "../lib/tauri";
|
import { rpc } from "../lib/rpc";
|
||||||
import { usePluginsKey } from "./usePlugins";
|
import { usePluginsKey } from "./usePlugins";
|
||||||
|
|
||||||
export type CallableFolderAction = Pick<FolderAction, "label" | "icon"> & {
|
export type CallableFolderAction = Pick<FolderAction, "label" | "icon"> & {
|
||||||
@@ -30,7 +30,7 @@ export function useFolderActions() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getFolderActions() {
|
export async function getFolderActions() {
|
||||||
const responses = await invokeCmd<GetFolderActionsResponse[]>("cmd_folder_actions");
|
const responses = await rpc<GetFolderActionsResponse[]>("cmd_folder_actions");
|
||||||
const actions = responses.flatMap((r) =>
|
const actions = responses.flatMap((r) =>
|
||||||
r.actions.map((a, i) => ({
|
r.actions.map((a, i) => ({
|
||||||
label: a.label,
|
label: a.label,
|
||||||
@@ -41,7 +41,7 @@ export async function getFolderActions() {
|
|||||||
pluginRefId: r.pluginRefId,
|
pluginRefId: r.pluginRefId,
|
||||||
args: { folder },
|
args: { folder },
|
||||||
};
|
};
|
||||||
await invokeCmd("cmd_call_folder_action", { req: payload });
|
await rpc("cmd_call_folder_action", { req: payload });
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { useKeyValue } from "./useKeyValue";
|
||||||
|
|
||||||
|
// The file a request's GraphQL schema is loaded from, or null when the schema
|
||||||
|
// comes from an introspection request.
|
||||||
|
//
|
||||||
|
// This is the *source*, not the schema. The introspection row it produces is a
|
||||||
|
// cache that expires on its own; this outlives it and regenerates it, the same
|
||||||
|
// way gRPC keeps its proto file list separate from a reflection result.
|
||||||
|
export function graphqlSchemaFileArgs(requestId: string | null) {
|
||||||
|
return {
|
||||||
|
namespace: "global" as const,
|
||||||
|
key: ["graphql_schema_file", requestId ?? "n/a"],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useGraphQLSchemaFile(requestId: string | null) {
|
||||||
|
return useKeyValue<string | null>({ ...graphqlSchemaFileArgs(requestId), fallback: null });
|
||||||
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
import { emit } from "@tauri-apps/api/event";
|
import { platform } from "@yaakapp-internal/platform";
|
||||||
import type { GrpcConnection, GrpcRequest } from "@yaakapp-internal/models";
|
import type { GrpcConnection, GrpcRequest } from "@yaakapp-internal/models";
|
||||||
import { flushAllModelWrites } from "@yaakapp-internal/models";
|
import { flushAllModelWrites } from "@yaakapp-internal/models";
|
||||||
import { jotaiStore } from "../lib/jotai";
|
import { jotaiStore } from "../lib/jotai";
|
||||||
import { minPromiseMillis } from "../lib/minPromiseMillis";
|
import { minPromiseMillis } from "../lib/minPromiseMillis";
|
||||||
import { invokeCmd } from "../lib/tauri";
|
import { rpc } from "../lib/rpc";
|
||||||
import { activeEnvironmentIdAtom, useActiveEnvironment } from "./useActiveEnvironment";
|
import { activeEnvironmentIdAtom, useActiveEnvironment } from "./useActiveEnvironment";
|
||||||
import { useDebouncedValue } from "@yaakapp-internal/ui";
|
import { useDebouncedValue } from "@yaakapp-internal/ui";
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ export function useGrpc(
|
|||||||
mutationKey: ["grpc_go", conn?.id],
|
mutationKey: ["grpc_go", conn?.id],
|
||||||
mutationFn: async () => {
|
mutationFn: async () => {
|
||||||
await flushAllModelWrites(); // The backend reads the request from the DB
|
await flushAllModelWrites(); // The backend reads the request from the DB
|
||||||
return invokeCmd<void>("cmd_grpc_go", {
|
return rpc<void>("cmd_grpc_go", {
|
||||||
requestId,
|
requestId,
|
||||||
environmentId: environment?.id,
|
environmentId: environment?.id,
|
||||||
protoFiles,
|
protoFiles,
|
||||||
@@ -36,17 +36,17 @@ export function useGrpc(
|
|||||||
const send = useMutation({
|
const send = useMutation({
|
||||||
mutationKey: ["grpc_send", conn?.id],
|
mutationKey: ["grpc_send", conn?.id],
|
||||||
mutationFn: ({ message }: { message: string }) =>
|
mutationFn: ({ message }: { message: string }) =>
|
||||||
emit(`grpc_client_msg_${conn?.id ?? "none"}`, { Message: message }),
|
platform.emit(`grpc_client_msg_${conn?.id ?? "none"}`, { Message: message }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const cancel = useMutation({
|
const cancel = useMutation({
|
||||||
mutationKey: ["grpc_cancel", conn?.id ?? "n/a"],
|
mutationKey: ["grpc_cancel", conn?.id ?? "n/a"],
|
||||||
mutationFn: () => emit(`grpc_client_msg_${conn?.id ?? "none"}`, "Cancel"),
|
mutationFn: () => platform.emit(`grpc_client_msg_${conn?.id ?? "none"}`, "Cancel"),
|
||||||
});
|
});
|
||||||
|
|
||||||
const commit = useMutation({
|
const commit = useMutation({
|
||||||
mutationKey: ["grpc_commit", conn?.id ?? "n/a"],
|
mutationKey: ["grpc_commit", conn?.id ?? "n/a"],
|
||||||
mutationFn: () => emit(`grpc_client_msg_${conn?.id ?? "none"}`, "Commit"),
|
mutationFn: () => platform.emit(`grpc_client_msg_${conn?.id ?? "none"}`, "Commit"),
|
||||||
});
|
});
|
||||||
|
|
||||||
const debouncedUrl = useDebouncedValue<string>(req?.url ?? "", 1000);
|
const debouncedUrl = useDebouncedValue<string>(req?.url ?? "", 1000);
|
||||||
@@ -61,7 +61,7 @@ export function useGrpc(
|
|||||||
queryFn: () => {
|
queryFn: () => {
|
||||||
const environmentId = jotaiStore.get(activeEnvironmentIdAtom);
|
const environmentId = jotaiStore.get(activeEnvironmentIdAtom);
|
||||||
return minPromiseMillis<ReflectResponseService[]>(
|
return minPromiseMillis<ReflectResponseService[]>(
|
||||||
invokeCmd("cmd_grpc_reflect", { requestId, protoFiles, environmentId }),
|
rpc("cmd_grpc_reflect", { requestId, protoFiles, environmentId }),
|
||||||
300,
|
300,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import type {
|
|||||||
GrpcRequestAction,
|
GrpcRequestAction,
|
||||||
} from "@yaakapp-internal/plugins";
|
} from "@yaakapp-internal/plugins";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { invokeCmd } from "../lib/tauri";
|
import { rpc } from "../lib/rpc";
|
||||||
import { getGrpcProtoFiles } from "./useGrpcProtoFiles";
|
import { getGrpcProtoFiles } from "./useGrpcProtoFiles";
|
||||||
import { usePluginsKey } from "./usePlugins";
|
import { usePluginsKey } from "./usePlugins";
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ export function useGrpcRequestActions() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getGrpcRequestActions() {
|
export async function getGrpcRequestActions() {
|
||||||
const responses = await invokeCmd<GetGrpcRequestActionsResponse[]>("cmd_grpc_request_actions");
|
const responses = await rpc<GetGrpcRequestActionsResponse[]>("cmd_grpc_request_actions");
|
||||||
|
|
||||||
return responses.flatMap((r) =>
|
return responses.flatMap((r) =>
|
||||||
r.actions.map((a, i) => ({
|
r.actions.map((a, i) => ({
|
||||||
@@ -46,7 +46,7 @@ export async function getGrpcRequestActions() {
|
|||||||
pluginRefId: r.pluginRefId,
|
pluginRefId: r.pluginRefId,
|
||||||
args: { grpcRequest, protoFiles },
|
args: { grpcRequest, protoFiles },
|
||||||
};
|
};
|
||||||
await invokeCmd("cmd_call_grpc_request_action", { req: payload });
|
await rpc("cmd_call_grpc_request_action", { req: payload });
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { type } from "@tauri-apps/plugin-os";
|
import { platform } from "@yaakapp-internal/platform";
|
||||||
import { debounce } from "@yaakapp-internal/lib";
|
import { debounce } from "@yaakapp-internal/lib";
|
||||||
import { settingsAtom } from "@yaakapp-internal/models";
|
import { settingsAtom } from "@yaakapp-internal/models";
|
||||||
import { atom, useAtomValue } from "jotai";
|
import { atom, useAtomValue } from "jotai";
|
||||||
@@ -102,7 +102,7 @@ const defaultHotkeysOther: Record<HotkeyAction, string[]> = {
|
|||||||
|
|
||||||
/** Get the default hotkeys for the current platform */
|
/** Get the default hotkeys for the current platform */
|
||||||
export const defaultHotkeys: Record<HotkeyAction, string[]> =
|
export const defaultHotkeys: Record<HotkeyAction, string[]> =
|
||||||
type() === "macos" ? defaultHotkeysMac : defaultHotkeysOther;
|
platform.osType() === "macos" ? defaultHotkeysMac : defaultHotkeysOther;
|
||||||
|
|
||||||
/** Atom that provides the effective hotkeys by merging defaults with user settings */
|
/** Atom that provides the effective hotkeys by merging defaults with user settings */
|
||||||
export const hotkeysAtom = atom((get) => {
|
export const hotkeysAtom = atom((get) => {
|
||||||
@@ -112,9 +112,12 @@ export const hotkeysAtom = atom((get) => {
|
|||||||
// Merge default hotkeys with custom hotkeys from settings
|
// Merge default hotkeys with custom hotkeys from settings
|
||||||
// Custom hotkeys override defaults for the same action
|
// Custom hotkeys override defaults for the same action
|
||||||
// An empty array means the hotkey is intentionally disabled
|
// An empty array means the hotkey is intentionally disabled
|
||||||
const merged: Record<HotkeyAction, string[]> = { ...defaultHotkeys };
|
const merged: Partial<Record<HotkeyAction, string[]>> = {};
|
||||||
|
for (const action of hotkeyActions) {
|
||||||
|
merged[action] = defaultHotkeys[action];
|
||||||
|
}
|
||||||
for (const [action, keys] of Object.entries(customHotkeys)) {
|
for (const [action, keys] of Object.entries(customHotkeys)) {
|
||||||
if (action in defaultHotkeys && Array.isArray(keys)) {
|
if (action in merged && Array.isArray(keys)) {
|
||||||
merged[action as HotkeyAction] = keys;
|
merged[action as HotkeyAction] = keys;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -122,7 +125,7 @@ export const hotkeysAtom = atom((get) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
/** Helper function to get current hotkeys from the store */
|
/** Helper function to get current hotkeys from the store */
|
||||||
function getHotkeys(): Record<HotkeyAction, string[]> {
|
function getHotkeys(): Partial<Record<HotkeyAction, string[]>> {
|
||||||
return jotaiStore.get(hotkeysAtom);
|
return jotaiStore.get(hotkeysAtom);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,9 +168,18 @@ const layoutInsensitiveKeys = [
|
|||||||
"Space",
|
"Space",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/** Zoom is the browser's own on these keys, so the app has no such action there. */
|
||||||
|
const ZOOM_ACTIONS: HotkeyAction[] = ["app.zoom_in", "app.zoom_out", "app.zoom_reset"];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The actions this host actually has. An action left out of here has no keys in
|
||||||
|
* `hotkeysAtom`, so it never matches and never claims the keystroke.
|
||||||
|
*/
|
||||||
export const hotkeyActions: HotkeyAction[] = (
|
export const hotkeyActions: HotkeyAction[] = (
|
||||||
Object.keys(defaultHotkeys) as (keyof typeof defaultHotkeys)[]
|
Object.keys(defaultHotkeys) as (keyof typeof defaultHotkeys)[]
|
||||||
).sort((a, b) => {
|
)
|
||||||
|
.filter((a) => platform.capabilities.interfaceZoom || !ZOOM_ACTIONS.includes(a))
|
||||||
|
.sort((a, b) => {
|
||||||
const scopeA = a.split(".")[0] || "";
|
const scopeA = a.split(".")[0] || "";
|
||||||
const scopeB = b.split(".")[0] || "";
|
const scopeB = b.split(".")[0] || "";
|
||||||
if (scopeA !== scopeB) {
|
if (scopeA !== scopeB) {
|
||||||
@@ -318,7 +330,7 @@ export function getHotkeyScope(action: HotkeyAction): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function formatHotkeyString(trigger: string): string[] {
|
export function formatHotkeyString(trigger: string): string[] {
|
||||||
const os = type();
|
const os = platform.osType();
|
||||||
const parts = trigger.split("+");
|
const parts = trigger.split("+");
|
||||||
const labelParts: string[] = [];
|
const labelParts: string[] = [];
|
||||||
|
|
||||||
@@ -333,7 +345,9 @@ export function formatHotkeyString(trigger: string): string[] {
|
|||||||
} else if (p === "Alt") {
|
} else if (p === "Alt") {
|
||||||
labelParts.push("⌥");
|
labelParts.push("⌥");
|
||||||
} else if (p === "Enter") {
|
} else if (p === "Enter") {
|
||||||
labelParts.push("↩");
|
// U+21A9 has an emoji presentation, which Chromium's font fallback picks
|
||||||
|
// (a blue glyph among monochrome ones). U+FE0E forces the text form.
|
||||||
|
labelParts.push("↩︎");
|
||||||
} else if (p === "Tab") {
|
} else if (p === "Tab") {
|
||||||
labelParts.push("⇥");
|
labelParts.push("⇥");
|
||||||
} else if (p === "Backspace") {
|
} else if (p === "Backspace") {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { GetHttpAuthenticationSummaryResponse } from "@yaakapp-internal/plu
|
|||||||
import { atom, useAtomValue } from "jotai";
|
import { atom, useAtomValue } from "jotai";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { jotaiStore } from "../lib/jotai";
|
import { jotaiStore } from "../lib/jotai";
|
||||||
import { invokeCmd } from "../lib/tauri";
|
import { rpc } from "../lib/rpc";
|
||||||
import { showErrorToast } from "../lib/toast";
|
import { showErrorToast } from "../lib/toast";
|
||||||
import { usePluginsKey } from "./usePlugins";
|
import { usePluginsKey } from "./usePlugins";
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ export function useSubscribeHttpAuthentication() {
|
|||||||
placeholderData: (prev) => prev, // Keep previous data on refetch
|
placeholderData: (prev) => prev, // Keep previous data on refetch
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
try {
|
try {
|
||||||
const result = await invokeCmd<GetHttpAuthenticationSummaryResponse[]>(
|
const result = await rpc<GetHttpAuthenticationSummaryResponse[]>(
|
||||||
"cmd_get_http_authentication_summaries",
|
"cmd_get_http_authentication_summaries",
|
||||||
);
|
);
|
||||||
setNumResults(result.length);
|
setNumResults(result.length);
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import type { GetHttpAuthenticationConfigResponse, JsonPrimitive } from "@yaakap
|
|||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import { md5 } from "js-md5";
|
import { md5 } from "js-md5";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { invokeCmd } from "../lib/tauri";
|
import { rpc } from "../lib/rpc";
|
||||||
import { activeEnvironmentIdAtom } from "./useActiveEnvironment";
|
import { activeEnvironmentIdAtom } from "./useActiveEnvironment";
|
||||||
import { activeWorkspaceIdAtom } from "./useActiveWorkspace";
|
import { activeWorkspaceIdAtom } from "./useActiveWorkspace";
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ export function useHttpAuthenticationConfig(
|
|||||||
placeholderData: (prev) => prev, // Keep previous data on refetch
|
placeholderData: (prev) => prev, // Keep previous data on refetch
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
if (authName == null || authName === "inherit") return null;
|
if (authName == null || authName === "inherit") return null;
|
||||||
const config = await invokeCmd<GetHttpAuthenticationConfigResponse>(
|
const config = await rpc<GetHttpAuthenticationConfigResponse>(
|
||||||
"cmd_get_http_authentication_config",
|
"cmd_get_http_authentication_config",
|
||||||
{
|
{
|
||||||
authName,
|
authName,
|
||||||
@@ -65,7 +65,7 @@ export function useHttpAuthenticationConfig(
|
|||||||
call: async (
|
call: async (
|
||||||
model: HttpRequest | GrpcRequest | WebsocketRequest | Folder | Workspace,
|
model: HttpRequest | GrpcRequest | WebsocketRequest | Folder | Workspace,
|
||||||
) => {
|
) => {
|
||||||
await invokeCmd("cmd_call_http_authentication_action", {
|
await rpc("cmd_call_http_authentication_action", {
|
||||||
pluginRefId: config.pluginRefId,
|
pluginRefId: config.pluginRefId,
|
||||||
actionIndex: i,
|
actionIndex: i,
|
||||||
authName,
|
authName,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import type {
|
|||||||
HttpRequestAction,
|
HttpRequestAction,
|
||||||
} from "@yaakapp-internal/plugins";
|
} from "@yaakapp-internal/plugins";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { invokeCmd } from "../lib/tauri";
|
import { rpc } from "../lib/rpc";
|
||||||
import { usePluginsKey } from "./usePlugins";
|
import { usePluginsKey } from "./usePlugins";
|
||||||
|
|
||||||
export type CallableHttpRequestAction = Pick<HttpRequestAction, "label" | "icon"> & {
|
export type CallableHttpRequestAction = Pick<HttpRequestAction, "label" | "icon"> & {
|
||||||
@@ -30,7 +30,7 @@ export function useHttpRequestActions() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getHttpRequestActions() {
|
export async function getHttpRequestActions() {
|
||||||
const responses = await invokeCmd<GetHttpRequestActionsResponse[]>("cmd_http_request_actions");
|
const responses = await rpc<GetHttpRequestActionsResponse[]>("cmd_http_request_actions");
|
||||||
const actions = responses.flatMap((r) =>
|
const actions = responses.flatMap((r) =>
|
||||||
r.actions.map((a, i) => ({
|
r.actions.map((a, i) => ({
|
||||||
label: a.label,
|
label: a.label,
|
||||||
@@ -41,7 +41,7 @@ export async function getHttpRequestActions() {
|
|||||||
pluginRefId: r.pluginRefId,
|
pluginRefId: r.pluginRefId,
|
||||||
args: { httpRequest },
|
args: { httpRequest },
|
||||||
};
|
};
|
||||||
await invokeCmd("cmd_call_http_request_action", { req: payload });
|
await rpc("cmd_call_http_request_action", { req: payload });
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import type { HttpResponse } from "@yaakapp-internal/models";
|
import type { HttpResponse } from "@yaakapp-internal/models";
|
||||||
import { invokeCmd } from "../lib/tauri";
|
import { rpc } from "../lib/rpc";
|
||||||
|
|
||||||
export function useHttpRequestBody(response: HttpResponse | null) {
|
export function useHttpRequestBody(response: HttpResponse | null) {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
@@ -18,7 +18,7 @@ export async function getRequestBodyText(response: HttpResponse | null) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await invokeCmd<number[] | null>("cmd_http_request_body", {
|
const data = await rpc<number[] | null>("cmd_http_request_body", {
|
||||||
responseId: response.id,
|
responseId: response.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { invoke } from "@tauri-apps/api/core";
|
|
||||||
import type { HttpResponse, HttpResponseEvent } from "@yaakapp-internal/models";
|
import type { HttpResponse, HttpResponseEvent } from "@yaakapp-internal/models";
|
||||||
import {
|
import {
|
||||||
httpResponseEventsAtom,
|
httpResponseEventsAtom,
|
||||||
@@ -8,6 +7,7 @@ import {
|
|||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { fireAndForget } from "../lib/fireAndForget";
|
import { fireAndForget } from "../lib/fireAndForget";
|
||||||
|
import { rpc } from "../lib/rpc";
|
||||||
|
|
||||||
export function useHttpResponseEvents(response: HttpResponse | null) {
|
export function useHttpResponseEvents(response: HttpResponse | null) {
|
||||||
const allEvents = useAtomValue(httpResponseEventsAtom);
|
const allEvents = useAtomValue(httpResponseEventsAtom);
|
||||||
@@ -20,7 +20,7 @@ export function useHttpResponseEvents(response: HttpResponse | null) {
|
|||||||
|
|
||||||
// Fetch events from database, filtering out events from other responses and merging atomically
|
// Fetch events from database, filtering out events from other responses and merging atomically
|
||||||
fireAndForget(
|
fireAndForget(
|
||||||
invoke<HttpResponseEvent[]>("cmd_get_http_response_events", { responseId: response.id }).then(
|
rpc<HttpResponseEvent[]>("cmd_get_http_response_events", { responseId: response.id }).then(
|
||||||
(events) =>
|
(events) =>
|
||||||
mergeModelsInStore("http_response_event", events, (e) => e.responseId === response.id),
|
mergeModelsInStore("http_response_event", events, (e) => e.responseId === response.id),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
import type { HttpRequest } from "@yaakapp-internal/models";
|
|
||||||
import { patchModelById } from "@yaakapp-internal/models";
|
|
||||||
import { createRequestAndNavigate } from "../lib/createRequestAndNavigate";
|
|
||||||
import { jotaiStore } from "../lib/jotai";
|
|
||||||
import { invokeCmd } from "../lib/tauri";
|
|
||||||
import { showToast } from "../lib/toast";
|
|
||||||
import { activeWorkspaceIdAtom } from "./useActiveWorkspace";
|
|
||||||
import { useFastMutation } from "./useFastMutation";
|
|
||||||
import { wasUpdatedExternally } from "./useRequestUpdateKey";
|
|
||||||
|
|
||||||
export function useImportCurl() {
|
|
||||||
return useFastMutation({
|
|
||||||
mutationKey: ["import_curl"],
|
|
||||||
mutationFn: async ({
|
|
||||||
overwriteRequestId,
|
|
||||||
command,
|
|
||||||
}: {
|
|
||||||
overwriteRequestId?: string;
|
|
||||||
command: string;
|
|
||||||
}) => {
|
|
||||||
const workspaceId = jotaiStore.get(activeWorkspaceIdAtom);
|
|
||||||
const importedRequest: HttpRequest = await invokeCmd("cmd_curl_to_request", {
|
|
||||||
command,
|
|
||||||
workspaceId,
|
|
||||||
});
|
|
||||||
|
|
||||||
let verb: string;
|
|
||||||
if (overwriteRequestId == null) {
|
|
||||||
verb = "Created";
|
|
||||||
await createRequestAndNavigate(importedRequest);
|
|
||||||
} else {
|
|
||||||
verb = "Updated";
|
|
||||||
await patchModelById(importedRequest.model, overwriteRequestId, (r: HttpRequest) => ({
|
|
||||||
...importedRequest,
|
|
||||||
id: r.id,
|
|
||||||
createdAt: r.createdAt,
|
|
||||||
workspaceId: r.workspaceId,
|
|
||||||
folderId: r.folderId,
|
|
||||||
name: r.name,
|
|
||||||
sortPriority: r.sortPriority,
|
|
||||||
}));
|
|
||||||
|
|
||||||
setTimeout(() => wasUpdatedExternally(overwriteRequestId), 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
showToast({
|
|
||||||
color: "success",
|
|
||||||
message: `${verb} request from Curl`,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,14 +1,16 @@
|
|||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
|
||||||
import type { GraphQlIntrospection, HttpRequest } from "@yaakapp-internal/models";
|
import type { GraphQlIntrospection, HttpRequest } from "@yaakapp-internal/models";
|
||||||
|
import { platform } from "@yaakapp-internal/platform";
|
||||||
import type { GraphQLSchema, IntrospectionQuery } from "graphql";
|
import type { GraphQLSchema, IntrospectionQuery } from "graphql";
|
||||||
import { buildClientSchema, getIntrospectionQuery } from "graphql";
|
import { buildClientSchema, getIntrospectionQuery } from "graphql";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
import { tryBuildIntrospectionFromFile } from "../lib/graphqlSchema";
|
||||||
import { minPromiseMillis } from "../lib/minPromiseMillis";
|
import { minPromiseMillis } from "../lib/minPromiseMillis";
|
||||||
import { getResponseBodyText } from "../lib/responseBody";
|
|
||||||
import { sendEphemeralRequest } from "../lib/sendEphemeralRequest";
|
import { sendEphemeralRequest } from "../lib/sendEphemeralRequest";
|
||||||
import { useActiveEnvironment } from "./useActiveEnvironment";
|
import { useActiveEnvironment } from "./useActiveEnvironment";
|
||||||
|
import { useGraphQLSchemaFile } from "./useGraphQLSchemaFile";
|
||||||
import { useDebouncedValue } from "@yaakapp-internal/ui";
|
import { useDebouncedValue } from "@yaakapp-internal/ui";
|
||||||
|
import { rpc } from "../lib/rpc";
|
||||||
|
|
||||||
const introspectionRequestBody = JSON.stringify({
|
const introspectionRequestBody = JSON.stringify({
|
||||||
query: getIntrospectionQuery(),
|
query: getIntrospectionQuery(),
|
||||||
@@ -30,9 +32,14 @@ export function useIntrospectGraphQL(
|
|||||||
|
|
||||||
const introspection = useIntrospectionResult(baseRequest);
|
const introspection = useIntrospectionResult(baseRequest);
|
||||||
|
|
||||||
|
// The schema's source. Outlives the introspection row it produces, so a
|
||||||
|
// request configured with a file keeps working after the row is swept.
|
||||||
|
const schemaFile = useGraphQLSchemaFile(baseRequest.id);
|
||||||
|
const filePath = schemaFile.value ?? null;
|
||||||
|
|
||||||
const upsertIntrospection = useCallback(
|
const upsertIntrospection = useCallback(
|
||||||
async (content: string | null) => {
|
async (content: string | null) => {
|
||||||
const v = await invoke<GraphQlIntrospection>("models_upsert_graphql_introspection", {
|
const v = await rpc<GraphQlIntrospection>("models_upsert_graphql_introspection", {
|
||||||
requestId: baseRequest.id,
|
requestId: baseRequest.id,
|
||||||
workspaceId: baseRequest.workspaceId,
|
workspaceId: baseRequest.workspaceId,
|
||||||
content: content ?? "",
|
content: content ?? "",
|
||||||
@@ -54,7 +61,7 @@ export function useIntrospectGraphQL(
|
|||||||
bodyType: "application/json",
|
bodyType: "application/json",
|
||||||
body: { text: introspectionRequestBody },
|
body: { text: introspectionRequestBody },
|
||||||
};
|
};
|
||||||
const response = await minPromiseMillis(
|
const { response, body } = await minPromiseMillis(
|
||||||
sendEphemeralRequest(args, activeEnvironment?.id ?? null),
|
sendEphemeralRequest(args, activeEnvironment?.id ?? null),
|
||||||
700,
|
700,
|
||||||
);
|
);
|
||||||
@@ -63,14 +70,16 @@ export function useIntrospectGraphQL(
|
|||||||
return setError(response.error);
|
return setError(response.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
const bodyText = await getResponseBodyText({ response, filter: null });
|
// The send hands back the only copy of the body — an unsaved response has
|
||||||
|
// nothing on disk and no row to read it back from
|
||||||
|
const bodyText = new TextDecoder("utf-8").decode(new Uint8Array(body));
|
||||||
if (response.status < 200 || response.status >= 300) {
|
if (response.status < 200 || response.status >= 300) {
|
||||||
return setError(
|
return setError(
|
||||||
`Request failed with status ${response.status}.\nThe response text is:\n\n${bodyText}`,
|
`Request failed with status ${response.status}.\nThe response text is:\n\n${bodyText}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bodyText === null) {
|
if (bodyText === "") {
|
||||||
return setError("Empty body returned in response");
|
return setError("Empty body returned in response");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,15 +99,109 @@ export function useIntrospectGraphQL(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
refetch().catch(console.error);
|
// A request pointed at a file gets its schema from that file. Introspecting
|
||||||
}, [baseRequest.id, debouncedRequest.url, debouncedRequest.method, activeEnvironment?.id]);
|
// here would overwrite it on the next URL edit.
|
||||||
|
if (filePath != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
refetch().catch(console.error);
|
||||||
|
}, [
|
||||||
|
baseRequest.id,
|
||||||
|
debouncedRequest.url,
|
||||||
|
debouncedRequest.method,
|
||||||
|
activeEnvironment?.id,
|
||||||
|
filePath,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Clears the schema, not the source. Removing a file source is a separate
|
||||||
|
// action, because the source is what would rebuild this a moment later.
|
||||||
const clear = useCallback(async () => {
|
const clear = useCallback(async () => {
|
||||||
setError("");
|
setError("");
|
||||||
setSchema(null);
|
setSchema(null);
|
||||||
await upsertIntrospection(null);
|
await upsertIntrospection(null);
|
||||||
}, [upsertIntrospection]);
|
}, [upsertIntrospection]);
|
||||||
|
|
||||||
|
// Reads a schema file and produces an introspection row from it, the same way
|
||||||
|
// `refetch` produces one from a server. Does not touch the stored source.
|
||||||
|
const introspectFromFile = useCallback(
|
||||||
|
async (path: string): Promise<{ ok: true } | { ok: false; error: string }> => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(undefined);
|
||||||
|
|
||||||
|
const fileContent = await platform.files.readText(path);
|
||||||
|
const result = tryBuildIntrospectionFromFile(fileContent);
|
||||||
|
|
||||||
|
if ("error" in result) {
|
||||||
|
setError(result.error);
|
||||||
|
return { ok: false, error: result.error };
|
||||||
|
}
|
||||||
|
|
||||||
|
await upsertIntrospection(result.content);
|
||||||
|
return { ok: true };
|
||||||
|
} catch (err) {
|
||||||
|
// The host rejects with a bare string for a missing or unreadable path,
|
||||||
|
// so this can't assume an Error.
|
||||||
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
|
setError(message);
|
||||||
|
return { ok: false, error: message };
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[upsertIntrospection],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Points the request at a file and immediately builds its schema from it.
|
||||||
|
const loadFromFile = useCallback(
|
||||||
|
async (path: string) => {
|
||||||
|
const result = await introspectFromFile(path);
|
||||||
|
if (result.ok) await schemaFile.set(path);
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
[introspectFromFile, schemaFile],
|
||||||
|
);
|
||||||
|
|
||||||
|
const reloadFromFile = useCallback(async () => {
|
||||||
|
if (filePath == null) return { ok: false as const, error: "No schema file to reload" };
|
||||||
|
return introspectFromFile(filePath);
|
||||||
|
}, [filePath, introspectFromFile]);
|
||||||
|
|
||||||
|
// The file-source counterpart of automatic introspection: re-read the file
|
||||||
|
// when the request is opened, so an edited schema is picked up without asking.
|
||||||
|
//
|
||||||
|
// A missing row is repaired even with the setting off — that is recovering
|
||||||
|
// from the 7-day sweep, not keeping the schema fresh, and skipping it would
|
||||||
|
// make the schema disappear with no visible cause.
|
||||||
|
const reloadedFor = useRef<string | null>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
if (filePath == null || introspection.isLoading) return;
|
||||||
|
// Only attempt once per path, so an unreadable file doesn't spin.
|
||||||
|
if (reloadedFor.current === filePath) return;
|
||||||
|
|
||||||
|
const hasContent = (introspection.data?.content ?? "") !== "";
|
||||||
|
if (hasContent && options.disabled) return;
|
||||||
|
|
||||||
|
reloadedFor.current = filePath;
|
||||||
|
introspectFromFile(filePath).catch(console.error);
|
||||||
|
}, [
|
||||||
|
filePath,
|
||||||
|
introspection.data?.content,
|
||||||
|
introspection.isLoading,
|
||||||
|
introspectFromFile,
|
||||||
|
options.disabled,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Stops using the file. The schema goes with it, since the file is what
|
||||||
|
// produced it; introspection repopulates if it's set to run automatically.
|
||||||
|
const removeSchemaFile = useCallback(async () => {
|
||||||
|
setError("");
|
||||||
|
setSchema(null);
|
||||||
|
await schemaFile.set(null);
|
||||||
|
await upsertIntrospection(null);
|
||||||
|
}, [schemaFile, upsertIntrospection]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (introspection.data?.content == null || introspection.data.content === "") {
|
if (introspection.data?.content == null || introspection.data.content === "") {
|
||||||
return;
|
return;
|
||||||
@@ -112,14 +215,24 @@ export function useIntrospectGraphQL(
|
|||||||
}
|
}
|
||||||
}, [introspection.data?.content]);
|
}, [introspection.data?.content]);
|
||||||
|
|
||||||
return { schema, isLoading, error, refetch, clear };
|
return {
|
||||||
|
schema,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
refetch,
|
||||||
|
clear,
|
||||||
|
loadFromFile,
|
||||||
|
reloadFromFile,
|
||||||
|
removeSchemaFile,
|
||||||
|
filePath,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function useIntrospectionResult(request: HttpRequest) {
|
function useIntrospectionResult(request: HttpRequest) {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ["introspection", request.id],
|
queryKey: ["introspection", request.id],
|
||||||
queryFn: async () =>
|
queryFn: async () =>
|
||||||
invoke<GraphQlIntrospection | null>("models_get_graphql_introspection", {
|
rpc<GraphQlIntrospection | null>("models_get_graphql_introspection", {
|
||||||
requestId: request.id,
|
requestId: request.id,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
import type { EventCallback, EventName } from "@tauri-apps/api/event";
|
|
||||||
import { listen } from "@tauri-apps/api/event";
|
|
||||||
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
|
|
||||||
import { useEffect, useRef } from "react";
|
|
||||||
|
|
||||||
export function useListenToTauriEvent<T>(event: EventName, fn: EventCallback<T>) {
|
|
||||||
const handlerRef = useRef(fn);
|
|
||||||
useEffect(() => {
|
|
||||||
handlerRef.current = fn;
|
|
||||||
}, [fn]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
return listenToTauriEvent<T>(event, (p) => handlerRef.current(p));
|
|
||||||
}, [event]);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function listenToTauriEvent<T>(event: EventName, fn: EventCallback<T>) {
|
|
||||||
const unsubPromise = listen<T>(
|
|
||||||
event,
|
|
||||||
fn,
|
|
||||||
// Listen to `emit_all()` events or events specific to the current window
|
|
||||||
{ target: { label: getCurrentWebviewWindow().label, kind: "Window" } },
|
|
||||||
);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
unsubPromise.then((unsub) => unsub()).catch(console.error);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import { invoke } from "@tauri-apps/api/core";
|
|
||||||
import type { GrpcConnection, GrpcEvent } from "@yaakapp-internal/models";
|
import type { GrpcConnection, GrpcEvent } from "@yaakapp-internal/models";
|
||||||
import {
|
import {
|
||||||
grpcConnectionsAtom,
|
grpcConnectionsAtom,
|
||||||
@@ -11,6 +10,7 @@ import { useEffect, useMemo } from "react";
|
|||||||
import { fireAndForget } from "../lib/fireAndForget";
|
import { fireAndForget } from "../lib/fireAndForget";
|
||||||
import { atomWithKVStorage } from "../lib/atoms/atomWithKVStorage";
|
import { atomWithKVStorage } from "../lib/atoms/atomWithKVStorage";
|
||||||
import { activeRequestIdAtom } from "./useActiveRequestId";
|
import { activeRequestIdAtom } from "./useActiveRequestId";
|
||||||
|
import { rpc } from "../lib/rpc";
|
||||||
|
|
||||||
const pinnedGrpcConnectionIdsAtom = atomWithKVStorage<Record<string, string | null>>(
|
const pinnedGrpcConnectionIdsAtom = atomWithKVStorage<Record<string, string | null>>(
|
||||||
"pinned-grpc-connection-ids",
|
"pinned-grpc-connection-ids",
|
||||||
@@ -71,7 +71,7 @@ export function useGrpcEvents(connectionId: string | null) {
|
|||||||
|
|
||||||
// Fetch events from database, filtering out events from other connections and merging atomically
|
// Fetch events from database, filtering out events from other connections and merging atomically
|
||||||
fireAndForget(
|
fireAndForget(
|
||||||
invoke<GrpcEvent[]>("models_grpc_events", { connectionId }).then((events) =>
|
rpc<GrpcEvent[]>("models_grpc_events", { connectionId }).then((events) =>
|
||||||
mergeModelsInStore("grpc_event", events, (e) => e.connectionId === connectionId),
|
mergeModelsInStore("grpc_event", events, (e) => e.connectionId === connectionId),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { invoke } from "@tauri-apps/api/core";
|
|
||||||
import type { WebsocketConnection, WebsocketEvent } from "@yaakapp-internal/models";
|
import type { WebsocketConnection, WebsocketEvent } from "@yaakapp-internal/models";
|
||||||
import {
|
import {
|
||||||
mergeModelsInStore,
|
mergeModelsInStore,
|
||||||
@@ -12,6 +11,7 @@ import { fireAndForget } from "../lib/fireAndForget";
|
|||||||
import { atomWithKVStorage } from "../lib/atoms/atomWithKVStorage";
|
import { atomWithKVStorage } from "../lib/atoms/atomWithKVStorage";
|
||||||
import { jotaiStore } from "../lib/jotai";
|
import { jotaiStore } from "../lib/jotai";
|
||||||
import { activeRequestIdAtom } from "./useActiveRequestId";
|
import { activeRequestIdAtom } from "./useActiveRequestId";
|
||||||
|
import { rpc } from "../lib/rpc";
|
||||||
|
|
||||||
const pinnedWebsocketConnectionIdAtom = atomWithKVStorage<Record<string, string | null>>(
|
const pinnedWebsocketConnectionIdAtom = atomWithKVStorage<Record<string, string | null>>(
|
||||||
"pinned-websocket-connection-ids",
|
"pinned-websocket-connection-ids",
|
||||||
@@ -58,7 +58,7 @@ export function useWebsocketEvents(connectionId: string | null) {
|
|||||||
|
|
||||||
// Fetch events from database, filtering out events from other connections and merging atomically
|
// Fetch events from database, filtering out events from other connections and merging atomically
|
||||||
fireAndForget(
|
fireAndForget(
|
||||||
invoke<WebsocketEvent[]>("models_websocket_events", { connectionId }).then((events) =>
|
rpc<WebsocketEvent[]>("models_websocket_events", { connectionId }).then((events) =>
|
||||||
mergeModelsInStore("websocket_event", events, (e) => e.connectionId === connectionId),
|
mergeModelsInStore("websocket_event", events, (e) => e.connectionId === connectionId),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { platform } from "@yaakapp-internal/platform";
|
||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
/** Subscribe to a backend event for the lifetime of the component. */
|
||||||
|
export function usePlatformEvent<T>(event: string, fn: (payload: T) => void) {
|
||||||
|
const handlerRef = useRef(fn);
|
||||||
|
useEffect(() => {
|
||||||
|
handlerRef.current = fn;
|
||||||
|
}, [fn]);
|
||||||
|
|
||||||
|
useEffect(() => platform.listen<T>(event, (payload) => handlerRef.current(payload)), [event]);
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ import { pluginsAtom } from "@yaakapp-internal/models";
|
|||||||
import type { PluginMetadata } from "@yaakapp-internal/plugins";
|
import type { PluginMetadata } from "@yaakapp-internal/plugins";
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import { queryClient } from "../lib/queryClient";
|
import { queryClient } from "../lib/queryClient";
|
||||||
import { invokeCmd } from "../lib/tauri";
|
import { rpc } from "../lib/rpc";
|
||||||
|
|
||||||
function pluginInfoKey(id: string | null, plugin: Plugin | null) {
|
function pluginInfoKey(id: string | null, plugin: Plugin | null) {
|
||||||
return ["plugin_info", id ?? "n/a", plugin?.updatedAt ?? "n/a"];
|
return ["plugin_info", id ?? "n/a", plugin?.updatedAt ?? "n/a"];
|
||||||
@@ -19,7 +19,7 @@ export function usePluginInfo(id: string | null) {
|
|||||||
placeholderData: (prev) => prev, // Keep previous data on refetch
|
placeholderData: (prev) => prev, // Keep previous data on refetch
|
||||||
queryFn: () => {
|
queryFn: () => {
|
||||||
if (id == null) return null;
|
if (id == null) return null;
|
||||||
return invokeCmd<PluginMetadata>("cmd_plugin_info", { id });
|
return rpc<PluginMetadata>("cmd_plugin_info", { id });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { changeModelStoreWorkspace, pluginsAtom } from "@yaakapp-internal/models
|
|||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import { jotaiStore } from "../lib/jotai";
|
import { jotaiStore } from "../lib/jotai";
|
||||||
import { minPromiseMillis } from "../lib/minPromiseMillis";
|
import { minPromiseMillis } from "../lib/minPromiseMillis";
|
||||||
import { invokeCmd } from "../lib/tauri";
|
import { rpc } from "../lib/rpc";
|
||||||
import { activeWorkspaceIdAtom } from "./useActiveWorkspace";
|
import { activeWorkspaceIdAtom } from "./useActiveWorkspace";
|
||||||
import { useDebouncedValue } from "@yaakapp-internal/ui";
|
import { useDebouncedValue } from "@yaakapp-internal/ui";
|
||||||
import { invalidateAllPluginInfo } from "./usePluginInfo";
|
import { invalidateAllPluginInfo } from "./usePluginInfo";
|
||||||
@@ -26,7 +26,7 @@ export function useRefreshPlugins() {
|
|||||||
mutationFn: async () => {
|
mutationFn: async () => {
|
||||||
await minPromiseMillis(
|
await minPromiseMillis(
|
||||||
(async () => {
|
(async () => {
|
||||||
await invokeCmd("cmd_reload_plugins");
|
await rpc("cmd_reload_plugins");
|
||||||
const workspaceId = jotaiStore.get(activeWorkspaceIdAtom);
|
const workspaceId = jotaiStore.get(activeWorkspaceIdAtom);
|
||||||
await changeModelStoreWorkspace(workspaceId); // Force refresh models
|
await changeModelStoreWorkspace(workspaceId); // Force refresh models
|
||||||
invalidateAllPluginInfo();
|
invalidateAllPluginInfo();
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import type { Appearance } from "@yaakapp-internal/theme";
|
import type { Appearance } from "@yaakapp-internal/theme";
|
||||||
import { getCSSAppearance, subscribeToPreferredAppearance } from "@yaakapp-internal/theme";
|
import {
|
||||||
|
getCSSAppearance,
|
||||||
|
getSystemAppearance,
|
||||||
|
subscribeToPreferredAppearance,
|
||||||
|
} from "@yaakapp-internal/theme";
|
||||||
|
|
||||||
export function usePreferredAppearance() {
|
export function usePreferredAppearance() {
|
||||||
const [preferredAppearance, setPreferredAppearance] = useState<Appearance>(getCSSAppearance());
|
const [preferredAppearance, setPreferredAppearance] = useState<Appearance>(
|
||||||
|
getSystemAppearance() ?? getCSSAppearance(),
|
||||||
|
);
|
||||||
useEffect(() => subscribeToPreferredAppearance(setPreferredAppearance), []);
|
useEffect(() => subscribeToPreferredAppearance(setPreferredAppearance), []);
|
||||||
return preferredAppearance;
|
return preferredAppearance;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { useCallback } from "react";
|
||||||
|
import { useKeyValue } from "./useKeyValue";
|
||||||
|
|
||||||
|
export interface RecentFilter {
|
||||||
|
value: string;
|
||||||
|
pinned?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAX_RECENT_FILTERS = 20;
|
||||||
|
const kvKey = (filterStateKey: string) => `recent_filters::${filterStateKey}`;
|
||||||
|
const namespace = "global";
|
||||||
|
const fallback: RecentFilter[] = [];
|
||||||
|
|
||||||
|
export function useRecentFilters(filterStateKey: string | null) {
|
||||||
|
const { value, set } = useKeyValue<RecentFilter[]>({
|
||||||
|
key: kvKey(filterStateKey ?? "n/a"),
|
||||||
|
namespace,
|
||||||
|
fallback,
|
||||||
|
});
|
||||||
|
|
||||||
|
const addFilter = useCallback(
|
||||||
|
async (rawValue: string) => {
|
||||||
|
const value = rawValue.trim();
|
||||||
|
if (filterStateKey == null || value === "") return;
|
||||||
|
await set((prev) => {
|
||||||
|
// Returning the same reference skips the write, so re-committing the
|
||||||
|
// expression already at the top (on every blur) costs nothing
|
||||||
|
if (prev[0]?.value === value) return prev;
|
||||||
|
const existing = prev.find((f) => f.value === value);
|
||||||
|
const rest = prev.filter((f) => f.value !== value);
|
||||||
|
return trim([{ value, pinned: existing?.pinned }, ...rest]);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[filterStateKey, set],
|
||||||
|
);
|
||||||
|
|
||||||
|
const removeFilter = useCallback(
|
||||||
|
async (value: string) => set((prev) => prev.filter((f) => f.value !== value)),
|
||||||
|
[set],
|
||||||
|
);
|
||||||
|
|
||||||
|
const togglePin = useCallback(
|
||||||
|
async (value: string) =>
|
||||||
|
set((prev) => prev.map((f) => (f.value === value ? { ...f, pinned: !f.pinned } : f))),
|
||||||
|
[set],
|
||||||
|
);
|
||||||
|
|
||||||
|
const clearFilters = useCallback(async () => set([]), [set]);
|
||||||
|
|
||||||
|
return { recentFilters: value ?? fallback, addFilter, removeFilter, togglePin, clearFilters };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Bound the list, evicting the oldest unpinned entries before any pinned ones */
|
||||||
|
function trim(filters: RecentFilter[]): RecentFilter[] {
|
||||||
|
const excess = filters.length - MAX_RECENT_FILTERS;
|
||||||
|
if (excess <= 0) return filters;
|
||||||
|
|
||||||
|
const evicted = new Set<number>();
|
||||||
|
for (let i = filters.length - 1; i >= 0 && evicted.size < excess; i--) {
|
||||||
|
if (!filters[i]?.pinned) evicted.add(i);
|
||||||
|
}
|
||||||
|
for (let i = filters.length - 1; i >= 0 && evicted.size < excess; i--) {
|
||||||
|
evicted.add(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
return filters.filter((_, i) => !evicted.has(i));
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ import { useQuery } from "@tanstack/react-query";
|
|||||||
import type { RenderPurpose } from "@yaakapp-internal/plugins";
|
import type { RenderPurpose } from "@yaakapp-internal/plugins";
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import { minPromiseMillis } from "../lib/minPromiseMillis";
|
import { minPromiseMillis } from "../lib/minPromiseMillis";
|
||||||
import { invokeCmd } from "../lib/tauri";
|
import { rpc } from "../lib/rpc";
|
||||||
import { useActiveEnvironment } from "./useActiveEnvironment";
|
import { useActiveEnvironment } from "./useActiveEnvironment";
|
||||||
import { activeWorkspaceIdAtom } from "./useActiveWorkspace";
|
import { activeWorkspaceIdAtom } from "./useActiveWorkspace";
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@ export async function renderTemplate({
|
|||||||
purpose: RenderPurpose;
|
purpose: RenderPurpose;
|
||||||
ignoreError?: boolean;
|
ignoreError?: boolean;
|
||||||
}): Promise<string> {
|
}): Promise<string> {
|
||||||
return invokeCmd("cmd_render_template", {
|
return rpc("cmd_render_template", {
|
||||||
template,
|
template,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
environmentId,
|
environmentId,
|
||||||
@@ -67,5 +67,5 @@ export async function decryptTemplate({
|
|||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
environmentId: string | null;
|
environmentId: string | null;
|
||||||
}): Promise<string> {
|
}): Promise<string> {
|
||||||
return invokeCmd("cmd_decrypt_template", { template, workspaceId, environmentId });
|
return rpc("cmd_decrypt_template", { template, workspaceId, environmentId });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
|
import { platform } from "@yaakapp-internal/platform";
|
||||||
import type { ModelPayload } from "@yaakapp-internal/models";
|
import type { ModelPayload } from "@yaakapp-internal/models";
|
||||||
import { atom, useAtomValue } from "jotai";
|
import { atom, useAtomValue } from "jotai";
|
||||||
import { generateId } from "../lib/generateId";
|
import { generateId } from "../lib/generateId";
|
||||||
@@ -6,8 +6,7 @@ import { jotaiStore } from "../lib/jotai";
|
|||||||
|
|
||||||
const requestUpdateKeyAtom = atom<Record<string, string>>({});
|
const requestUpdateKeyAtom = atom<Record<string, string>>({});
|
||||||
|
|
||||||
getCurrentWebviewWindow()
|
platform.listen<ModelPayload[]>("model_writes", (payloads) => {
|
||||||
.listen<ModelPayload[]>("model_writes", ({ payload: payloads }) => {
|
|
||||||
const changedIds: string[] = [];
|
const changedIds: string[] = [];
|
||||||
for (const payload of payloads) {
|
for (const payload of payloads) {
|
||||||
if (payload.change.type !== "upsert") continue;
|
if (payload.change.type !== "upsert") continue;
|
||||||
@@ -17,15 +16,14 @@ getCurrentWebviewWindow()
|
|||||||
payload.model.model === "grpc_request" ||
|
payload.model.model === "grpc_request" ||
|
||||||
payload.model.model === "websocket_request") &&
|
payload.model.model === "websocket_request") &&
|
||||||
((payload.updateSource.type === "window" &&
|
((payload.updateSource.type === "window" &&
|
||||||
payload.updateSource.label !== getCurrentWebviewWindow().label) ||
|
payload.updateSource.label !== platform.window.label) ||
|
||||||
payload.updateSource.type !== "window")
|
payload.updateSource.type !== "window")
|
||||||
) {
|
) {
|
||||||
changedIds.push(payload.model.id);
|
changedIds.push(payload.model.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (changedIds.length > 0) wasUpdatedExternally(changedIds);
|
if (changedIds.length > 0) wasUpdatedExternally(changedIds);
|
||||||
})
|
});
|
||||||
.catch(console.error);
|
|
||||||
|
|
||||||
export function wasUpdatedExternally(changedRequestIds: string | string[]) {
|
export function wasUpdatedExternally(changedRequestIds: string | string[]) {
|
||||||
const ids = Array.isArray(changedRequestIds) ? changedRequestIds : [changedRequestIds];
|
const ids = Array.isArray(changedRequestIds) ? changedRequestIds : [changedRequestIds];
|
||||||
|
|||||||
@@ -2,6 +2,25 @@ import { useQuery } from "@tanstack/react-query";
|
|||||||
import type { HttpResponse } from "@yaakapp-internal/models";
|
import type { HttpResponse } from "@yaakapp-internal/models";
|
||||||
import { getResponseBodyBytes, getResponseBodyText } from "../lib/responseBody";
|
import { getResponseBodyBytes, getResponseBodyText } from "../lib/responseBody";
|
||||||
|
|
||||||
|
export function responseBodyTextQuery({
|
||||||
|
response,
|
||||||
|
filter,
|
||||||
|
}: {
|
||||||
|
response: HttpResponse;
|
||||||
|
filter: string | null;
|
||||||
|
}) {
|
||||||
|
return {
|
||||||
|
queryKey: [
|
||||||
|
"response_body_text",
|
||||||
|
response.id,
|
||||||
|
response.updatedAt,
|
||||||
|
response.contentLength,
|
||||||
|
filter ?? "",
|
||||||
|
],
|
||||||
|
queryFn: () => getResponseBodyText({ response, filter }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function useResponseBodyText({
|
export function useResponseBodyText({
|
||||||
response,
|
response,
|
||||||
filter,
|
filter,
|
||||||
@@ -11,14 +30,7 @@ export function useResponseBodyText({
|
|||||||
}) {
|
}) {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
placeholderData: (prev) => prev, // Keep previous data on refetch
|
placeholderData: (prev) => prev, // Keep previous data on refetch
|
||||||
queryKey: [
|
...responseBodyTextQuery({ response, filter }),
|
||||||
"response_body_text",
|
|
||||||
response.id,
|
|
||||||
response.updatedAt,
|
|
||||||
response.contentLength,
|
|
||||||
filter ?? "",
|
|
||||||
],
|
|
||||||
queryFn: () => getResponseBodyText({ response, filter }),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import type { HttpResponse } from "@yaakapp-internal/models";
|
||||||
|
import { platform } from "@yaakapp-internal/platform";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A URL for a stored response body, for the viewers that hand one to an element
|
||||||
|
* instead of reading the bytes themselves.
|
||||||
|
*
|
||||||
|
* Resolved once here rather than inside each viewer: the host may have to ask
|
||||||
|
* the backend where the body is, and a viewer that computes its source during
|
||||||
|
* render (the PDF one, deliberately) needs it settled before it mounts.
|
||||||
|
*
|
||||||
|
* Null data means the response has no stored body.
|
||||||
|
*/
|
||||||
|
export function useResponseBodyUrl(response: HttpResponse | null) {
|
||||||
|
const responseId = response?.id ?? null;
|
||||||
|
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["response_body_url", responseId, response?.updatedAt ?? ""],
|
||||||
|
enabled: responseId != null,
|
||||||
|
// A response body is stored under the response's own id
|
||||||
|
queryFn: () => (responseId == null ? null : platform.blobs.url(responseId)),
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
import { useCallback, useState } from "react";
|
||||||
|
import { createGlobalState } from "react-use";
|
||||||
|
import type { RecentFilter } from "./useRecentFilters";
|
||||||
|
import { useRecentFilters } from "./useRecentFilters";
|
||||||
|
|
||||||
|
/** What's typed in the filter box. `null` means the filter box is closed */
|
||||||
|
const useFilterTextMap = createGlobalState<Record<string, string | null>>({});
|
||||||
|
|
||||||
|
/** What's actually applied to the response. Only changes on an explicit apply */
|
||||||
|
const useAppliedFilterMap = createGlobalState<Record<string, string | null>>({});
|
||||||
|
|
||||||
|
export interface ResponseFilterApi {
|
||||||
|
stateKey: string | null;
|
||||||
|
/** Draft text in the filter box, or `null` when the box is closed */
|
||||||
|
filterText: string | null;
|
||||||
|
/** The expression currently filtering the response */
|
||||||
|
appliedFilter: string | null;
|
||||||
|
isSearching: boolean;
|
||||||
|
/** The box holds an expression that isn't the one currently applied */
|
||||||
|
isDirty: boolean;
|
||||||
|
/** Bumped when the (uncontrolled) filter input must re-read its defaultValue */
|
||||||
|
filterUpdateKey: number;
|
||||||
|
setFilterText: (value: string | null) => void;
|
||||||
|
/** Apply the expression to the response, recording it if the filter accepts it */
|
||||||
|
applyFilter: (value: string) => void;
|
||||||
|
/** Like applyFilter, but also replaces what's shown in the filter box */
|
||||||
|
replaceFilter: (value: string) => void;
|
||||||
|
toggleSearch: () => void;
|
||||||
|
recentFilters: RecentFilter[];
|
||||||
|
removeRecentFilter: (value: string) => void;
|
||||||
|
togglePinRecentFilter: (value: string) => void;
|
||||||
|
clearRecentFilters: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draft/applied state and history for a response filter (JSONPath/XPath).
|
||||||
|
*
|
||||||
|
* History records at most one entry per apply gesture, and only after `runFilter`
|
||||||
|
* confirms the plugin accepts the expression — the plugin is the sole judge of
|
||||||
|
* validity. Because nothing but the gesture ever writes, refetches can't resurrect
|
||||||
|
* deleted entries and a gesture can't record into another request's history.
|
||||||
|
*/
|
||||||
|
export function useResponseFilter({
|
||||||
|
stateKey,
|
||||||
|
runFilter,
|
||||||
|
}: {
|
||||||
|
stateKey: string | null;
|
||||||
|
/** Evaluate an expression, rejecting if the filter plugin reports an error */
|
||||||
|
runFilter: (filter: string) => Promise<unknown>;
|
||||||
|
}): ResponseFilterApi {
|
||||||
|
const [filterTextMap, setFilterTextMap] = useFilterTextMap();
|
||||||
|
const [appliedFilterMap, setAppliedFilterMap] = useAppliedFilterMap();
|
||||||
|
const filterText = stateKey ? (filterTextMap[stateKey] ?? null) : null;
|
||||||
|
const appliedFilter = stateKey ? (appliedFilterMap[stateKey] ?? null) : null;
|
||||||
|
|
||||||
|
const setFilterText = useCallback(
|
||||||
|
(v: string | null) => {
|
||||||
|
if (!stateKey) return;
|
||||||
|
setFilterTextMap((m) => ({ ...m, [stateKey]: v }));
|
||||||
|
},
|
||||||
|
[stateKey, setFilterTextMap],
|
||||||
|
);
|
||||||
|
|
||||||
|
const setAppliedFilter = useCallback(
|
||||||
|
(v: string | null) => {
|
||||||
|
if (!stateKey) return;
|
||||||
|
setAppliedFilterMap((m) => ({ ...m, [stateKey]: v }));
|
||||||
|
},
|
||||||
|
[stateKey, setAppliedFilterMap],
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
recentFilters,
|
||||||
|
addFilter,
|
||||||
|
removeFilter: removeRecentFilter,
|
||||||
|
togglePin: togglePinRecentFilter,
|
||||||
|
clearFilters: clearRecentFilters,
|
||||||
|
} = useRecentFilters(stateKey);
|
||||||
|
|
||||||
|
const applyFilter = useCallback(
|
||||||
|
(value: string) => {
|
||||||
|
setFilterText(value);
|
||||||
|
const applied = value.trim() === "" ? null : value.trim();
|
||||||
|
setAppliedFilter(applied);
|
||||||
|
if (applied == null) return;
|
||||||
|
runFilter(applied).then(
|
||||||
|
() => addFilter(applied),
|
||||||
|
() => {}, // Rejected by the filter plugin — don't record
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[setFilterText, setAppliedFilter, runFilter, addFilter],
|
||||||
|
);
|
||||||
|
|
||||||
|
const [filterUpdateKey, setFilterUpdateKey] = useState(0);
|
||||||
|
const replaceFilter = useCallback(
|
||||||
|
(value: string) => {
|
||||||
|
applyFilter(value);
|
||||||
|
setFilterUpdateKey((k) => k + 1);
|
||||||
|
},
|
||||||
|
[applyFilter],
|
||||||
|
);
|
||||||
|
|
||||||
|
const isSearching = filterText != null;
|
||||||
|
const toggleSearch = useCallback(() => {
|
||||||
|
if (isSearching) {
|
||||||
|
setFilterText(null);
|
||||||
|
setAppliedFilter(null);
|
||||||
|
} else {
|
||||||
|
setFilterText("");
|
||||||
|
}
|
||||||
|
}, [isSearching, setFilterText, setAppliedFilter]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
stateKey,
|
||||||
|
filterText,
|
||||||
|
appliedFilter,
|
||||||
|
isSearching,
|
||||||
|
isDirty: filterText != null && filterText.trim() !== (appliedFilter ?? ""),
|
||||||
|
filterUpdateKey,
|
||||||
|
setFilterText,
|
||||||
|
applyFilter,
|
||||||
|
replaceFilter,
|
||||||
|
toggleSearch,
|
||||||
|
recentFilters,
|
||||||
|
removeRecentFilter,
|
||||||
|
togglePinRecentFilter,
|
||||||
|
clearRecentFilters,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
import { save } from "@tauri-apps/plugin-dialog";
|
|
||||||
import type { HttpResponse } from "@yaakapp-internal/models";
|
import type { HttpResponse } from "@yaakapp-internal/models";
|
||||||
import { getModel } from "@yaakapp-internal/models";
|
import { getModel } from "@yaakapp-internal/models";
|
||||||
import mime from "mime";
|
import mime from "mime";
|
||||||
import slugify from "slugify";
|
import slugify from "slugify";
|
||||||
import { InlineCode } from "@yaakapp-internal/ui";
|
import { InlineCode } from "@yaakapp-internal/ui";
|
||||||
import { getContentTypeFromHeaders } from "../lib/model_util";
|
import { getContentTypeFromHeaders } from "../lib/model_util";
|
||||||
import { invokeCmd } from "../lib/tauri";
|
import { rpc } from "../lib/rpc";
|
||||||
import { showToast } from "../lib/toast";
|
import { showToast } from "../lib/toast";
|
||||||
import { useFastMutation } from "./useFastMutation";
|
import { useFastMutation } from "./useFastMutation";
|
||||||
|
import { platform } from "@yaakapp-internal/platform";
|
||||||
|
|
||||||
export function useSaveResponse(response: HttpResponse | null) {
|
export function useSaveResponse(response: HttpResponse | null) {
|
||||||
return useFastMutation({
|
return useFastMutation({
|
||||||
@@ -21,11 +21,15 @@ export function useSaveResponse(response: HttpResponse | null) {
|
|||||||
const contentType = getContentTypeFromHeaders(response.headers) ?? "unknown";
|
const contentType = getContentTypeFromHeaders(response.headers) ?? "unknown";
|
||||||
const ext = mime.getExtension(contentType);
|
const ext = mime.getExtension(contentType);
|
||||||
const slug = slugify(request.name || "response", { lower: true });
|
const slug = slugify(request.name || "response", { lower: true });
|
||||||
const filepath = await save({
|
const filepath = await platform.dialog.save({
|
||||||
defaultPath: ext ? `${slug}.${ext}` : slug,
|
defaultPath: ext ? `${slug}.${ext}` : slug,
|
||||||
title: "Save Response",
|
title: "Save Response",
|
||||||
});
|
});
|
||||||
await invokeCmd("cmd_save_response", { responseId: response.id, filepath });
|
if (filepath == null) {
|
||||||
|
return; // Cancelled
|
||||||
|
}
|
||||||
|
|
||||||
|
await rpc("cmd_save_response", { responseId: response.id, filepath });
|
||||||
showToast({
|
showToast({
|
||||||
message: (
|
message: (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { HttpResponse } from "@yaakapp-internal/models";
|
import type { HttpResponse } from "@yaakapp-internal/models";
|
||||||
import { flushAllModelWrites } from "@yaakapp-internal/models";
|
import { flushAllModelWrites } from "@yaakapp-internal/models";
|
||||||
import { invokeCmd } from "../lib/tauri";
|
import { rpc } from "../lib/rpc";
|
||||||
import { getActiveCookieJar } from "./useActiveCookieJar";
|
import { getActiveCookieJar } from "./useActiveCookieJar";
|
||||||
import { getActiveEnvironment } from "./useActiveEnvironment";
|
import { getActiveEnvironment } from "./useActiveEnvironment";
|
||||||
import { createFastMutation, useFastMutation } from "./useFastMutation";
|
import { createFastMutation, useFastMutation } from "./useFastMutation";
|
||||||
@@ -12,7 +12,7 @@ async function sendAnyHttpRequestById(id: string | null): Promise<HttpResponse |
|
|||||||
|
|
||||||
await flushAllModelWrites();
|
await flushAllModelWrites();
|
||||||
|
|
||||||
return invokeCmd("cmd_send_http_request", {
|
return rpc("cmd_send_http_request", {
|
||||||
requestId: id,
|
requestId: id,
|
||||||
environmentId: getActiveEnvironment()?.id,
|
environmentId: getActiveEnvironment()?.id,
|
||||||
cookieJarId: getActiveCookieJar()?.id,
|
cookieJarId: getActiveCookieJar()?.id,
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { type } from "@tauri-apps/plugin-os";
|
import { platform } from "@yaakapp-internal/platform";
|
||||||
import { useIsFullscreen } from "@yaakapp-internal/ui";
|
import { useIsFullscreen } from "@yaakapp-internal/ui";
|
||||||
|
|
||||||
export function useStoplightsVisible() {
|
export function useStoplightsVisible() {
|
||||||
const fullscreen = useIsFullscreen();
|
const fullscreen = useIsFullscreen();
|
||||||
const stoplightsVisible = type() === "macos" && !fullscreen;
|
const stoplightsVisible = platform.osType() === "macos" && !fullscreen;
|
||||||
return stoplightsVisible;
|
return stoplightsVisible;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
|
import { platform } from "@yaakapp-internal/platform";
|
||||||
import { settingsAtom } from "@yaakapp-internal/models";
|
import { settingsAtom } from "@yaakapp-internal/models";
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
@@ -11,7 +11,7 @@ export function useSyncFontSizeSetting() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { interfaceScale, editorFontSize } = settings;
|
const { interfaceScale, editorFontSize } = settings;
|
||||||
getCurrentWebviewWindow().setZoom(interfaceScale).catch(console.error);
|
platform.window.setZoom(interfaceScale).catch(console.error);
|
||||||
document.documentElement.style.setProperty("--editor-font-size", `${editorFontSize}px`);
|
document.documentElement.style.setProperty("--editor-font-size", `${editorFontSize}px`);
|
||||||
}, [settings]);
|
}, [settings]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useHotKey } from "./useHotKey";
|
import { useHotKey } from "./useHotKey";
|
||||||
import { useListenToTauriEvent } from "./useListenToTauriEvent";
|
import { usePlatformEvent } from "./usePlatformEvent";
|
||||||
import { useZoom } from "./useZoom";
|
import { useZoom } from "./useZoom";
|
||||||
|
|
||||||
export function useSyncZoomSetting() {
|
export function useSyncZoomSetting() {
|
||||||
@@ -8,9 +8,9 @@ export function useSyncZoomSetting() {
|
|||||||
// shortcuts for Windows/Linux
|
// shortcuts for Windows/Linux
|
||||||
const zoom = useZoom();
|
const zoom = useZoom();
|
||||||
useHotKey("app.zoom_in", zoom.zoomIn);
|
useHotKey("app.zoom_in", zoom.zoomIn);
|
||||||
useListenToTauriEvent("zoom_in", zoom.zoomIn);
|
usePlatformEvent("zoom_in", zoom.zoomIn);
|
||||||
useHotKey("app.zoom_out", zoom.zoomOut);
|
useHotKey("app.zoom_out", zoom.zoomOut);
|
||||||
useListenToTauriEvent("zoom_out", zoom.zoomOut);
|
usePlatformEvent("zoom_out", zoom.zoomOut);
|
||||||
useHotKey("app.zoom_reset", zoom.zoomReset);
|
useHotKey("app.zoom_reset", zoom.zoomReset);
|
||||||
useListenToTauriEvent("zoom_reset", zoom.zoomReset);
|
usePlatformEvent("zoom_reset", zoom.zoomReset);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
import type { GetTemplateFunctionConfigResponse, JsonPrimitive } from "@yaakapp-internal/plugins";
|
import type { GetTemplateFunctionConfigResponse, JsonPrimitive } from "@yaakapp-internal/plugins";
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import { md5 } from "js-md5";
|
import { md5 } from "js-md5";
|
||||||
import { invokeCmd } from "../lib/tauri";
|
import { rpc } from "../lib/rpc";
|
||||||
import { activeEnvironmentIdAtom } from "./useActiveEnvironment";
|
import { activeEnvironmentIdAtom } from "./useActiveEnvironment";
|
||||||
import { activeWorkspaceIdAtom } from "./useActiveWorkspace";
|
import { activeWorkspaceIdAtom } from "./useActiveWorkspace";
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ export async function getTemplateFunctionConfig(
|
|||||||
model: HttpRequest | GrpcRequest | WebsocketRequest | Folder | Workspace,
|
model: HttpRequest | GrpcRequest | WebsocketRequest | Folder | Workspace,
|
||||||
environmentId: string | undefined,
|
environmentId: string | undefined,
|
||||||
) {
|
) {
|
||||||
const config = await invokeCmd<GetTemplateFunctionConfigResponse>(
|
const config = await rpc<GetTemplateFunctionConfigResponse>(
|
||||||
"cmd_template_function_config",
|
"cmd_template_function_config",
|
||||||
{
|
{
|
||||||
functionName,
|
functionName,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import type {
|
|||||||
import { atom, useAtomValue, useSetAtom } from "jotai";
|
import { atom, useAtomValue, useSetAtom } from "jotai";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import type { TwigCompletionOption } from "../components/core/Editor/twig/completion";
|
import type { TwigCompletionOption } from "../components/core/Editor/twig/completion";
|
||||||
import { invokeCmd } from "../lib/tauri";
|
import { rpc } from "../lib/rpc";
|
||||||
import { usePluginsKey } from "./usePlugins";
|
import { usePluginsKey } from "./usePlugins";
|
||||||
|
|
||||||
const templateFunctionsAtom = atom<TemplateFunction[]>([]);
|
const templateFunctionsAtom = atom<TemplateFunction[]>([]);
|
||||||
@@ -49,7 +49,7 @@ export function useSubscribeTemplateFunctions() {
|
|||||||
refetchInterval: numFns > 0 ? Number.POSITIVE_INFINITY : 1000,
|
refetchInterval: numFns > 0 ? Number.POSITIVE_INFINITY : 1000,
|
||||||
refetchOnMount: true,
|
refetchOnMount: true,
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const result = await invokeCmd<GetTemplateFunctionSummaryResponse[]>(
|
const result = await rpc<GetTemplateFunctionSummaryResponse[]>(
|
||||||
"cmd_template_function_summaries",
|
"cmd_template_function_summaries",
|
||||||
);
|
);
|
||||||
setNumFns(result.length);
|
setNumFns(result.length);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import type { Tokens } from "@yaakapp-internal/templates";
|
import type { Tokens } from "@yaakapp-internal/templates";
|
||||||
import { invokeCmd } from "../lib/tauri";
|
import { rpc } from "../lib/rpc";
|
||||||
|
|
||||||
export function useTemplateTokensToString(tokens: Tokens) {
|
export function useTemplateTokensToString(tokens: Tokens) {
|
||||||
return useQuery<string>({
|
return useQuery<string>({
|
||||||
@@ -11,5 +11,5 @@ export function useTemplateTokensToString(tokens: Tokens) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function templateTokensToString(tokens: Tokens): Promise<string> {
|
export async function templateTokensToString(tokens: Tokens): Promise<string> {
|
||||||
return invokeCmd("cmd_template_tokens_to_string", { tokens });
|
return rpc("cmd_template_tokens_to_string", { tokens });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import type {
|
|||||||
WebsocketRequestAction,
|
WebsocketRequestAction,
|
||||||
} from "@yaakapp-internal/plugins";
|
} from "@yaakapp-internal/plugins";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { invokeCmd } from "../lib/tauri";
|
import { rpc } from "../lib/rpc";
|
||||||
import { usePluginsKey } from "./usePlugins";
|
import { usePluginsKey } from "./usePlugins";
|
||||||
|
|
||||||
export type CallableWebSocketRequestAction = Pick<WebsocketRequestAction, "label" | "icon"> & {
|
export type CallableWebSocketRequestAction = Pick<WebsocketRequestAction, "label" | "icon"> & {
|
||||||
@@ -30,7 +30,7 @@ export function useWebsocketRequestActions() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getWebsocketRequestActions() {
|
export async function getWebsocketRequestActions() {
|
||||||
const responses = await invokeCmd<GetWebsocketRequestActionsResponse[]>(
|
const responses = await rpc<GetWebsocketRequestActionsResponse[]>(
|
||||||
"cmd_websocket_request_actions",
|
"cmd_websocket_request_actions",
|
||||||
);
|
);
|
||||||
const actions = responses.flatMap((r) =>
|
const actions = responses.flatMap((r) =>
|
||||||
@@ -43,7 +43,7 @@ export async function getWebsocketRequestActions() {
|
|||||||
pluginRefId: r.pluginRefId,
|
pluginRefId: r.pluginRefId,
|
||||||
args: { websocketRequest },
|
args: { websocketRequest },
|
||||||
};
|
};
|
||||||
await invokeCmd("cmd_call_websocket_request_action", { req: payload });
|
await rpc("cmd_call_websocket_request_action", { req: payload });
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,18 +1,11 @@
|
|||||||
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
|
import { platform } from "@yaakapp-internal/platform";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { fireAndForget } from "../lib/fireAndForget";
|
|
||||||
|
|
||||||
export function useWindowFocus() {
|
export function useWindowFocus() {
|
||||||
const [visible, setVisible] = useState(true);
|
const [visible, setVisible] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unlisten = getCurrentWebviewWindow().onFocusChanged((e) => {
|
return platform.window.onFocusChanged(setVisible);
|
||||||
setVisible(e.payload);
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
fireAndForget(unlisten.then((fn) => fn()));
|
|
||||||
};
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return visible;
|
return visible;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import type {
|
|||||||
WorkspaceAction,
|
WorkspaceAction,
|
||||||
} from "@yaakapp-internal/plugins";
|
} from "@yaakapp-internal/plugins";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { invokeCmd } from "../lib/tauri";
|
import { rpc } from "../lib/rpc";
|
||||||
import { usePluginsKey } from "./usePlugins";
|
import { usePluginsKey } from "./usePlugins";
|
||||||
|
|
||||||
export type CallableWorkspaceAction = Pick<WorkspaceAction, "label" | "icon"> & {
|
export type CallableWorkspaceAction = Pick<WorkspaceAction, "label" | "icon"> & {
|
||||||
@@ -30,7 +30,7 @@ export function useWorkspaceActions() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getWorkspaceActions() {
|
export async function getWorkspaceActions() {
|
||||||
const responses = await invokeCmd<GetWorkspaceActionsResponse[]>("cmd_workspace_actions");
|
const responses = await rpc<GetWorkspaceActionsResponse[]>("cmd_workspace_actions");
|
||||||
const actions = responses.flatMap((r) =>
|
const actions = responses.flatMap((r) =>
|
||||||
r.actions.map((a, i) => ({
|
r.actions.map((a, i) => ({
|
||||||
label: a.label,
|
label: a.label,
|
||||||
@@ -41,7 +41,7 @@ export async function getWorkspaceActions() {
|
|||||||
pluginRefId: r.pluginRefId,
|
pluginRefId: r.pluginRefId,
|
||||||
args: { workspace },
|
args: { workspace },
|
||||||
};
|
};
|
||||||
await invokeCmd("cmd_call_workspace_action", { req: payload });
|
await rpc("cmd_call_workspace_action", { req: payload });
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { AnyModel, ModelPayload } from "@yaakapp-internal/models";
|
|||||||
import { watchWorkspaceFiles } from "@yaakapp-internal/sync";
|
import { watchWorkspaceFiles } from "@yaakapp-internal/sync";
|
||||||
import { syncWorkspace } from "../commands/commands";
|
import { syncWorkspace } from "../commands/commands";
|
||||||
import { activeWorkspaceIdAtom, activeWorkspaceMetaAtom } from "../hooks/useActiveWorkspace";
|
import { activeWorkspaceIdAtom, activeWorkspaceMetaAtom } from "../hooks/useActiveWorkspace";
|
||||||
import { listenToTauriEvent } from "../hooks/useListenToTauriEvent";
|
import { platform } from "@yaakapp-internal/platform";
|
||||||
import { jotaiStore } from "../lib/jotai";
|
import { jotaiStore } from "../lib/jotai";
|
||||||
|
|
||||||
export function initSync() {
|
export function initSync() {
|
||||||
@@ -33,8 +33,8 @@ const syncAfterModelWrite = eagerDebounceAsync(sync, 1000);
|
|||||||
* simply add long-lived subscribers for the lifetime of the app.
|
* simply add long-lived subscribers for the lifetime of the app.
|
||||||
*/
|
*/
|
||||||
function initModelListeners() {
|
function initModelListeners() {
|
||||||
listenToTauriEvent<ModelPayload[]>("model_writes", (p) => {
|
platform.listen<ModelPayload[]>("model_writes", (payloads) => {
|
||||||
if (p.payload.some((payload) => isModelRelevant(payload.model))) syncAfterModelWrite();
|
if (payloads.some((payload) => isModelRelevant(payload.model))) syncAfterModelWrite();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user