Compare commits

..

51 Commits

Author SHA1 Message Date
Gregory Schier
e1363cf151 Make protoc writable before signing 2024-11-23 06:18:17 -08:00
Gregory Schier
38e0f5ede7 Remove unnecessary things in Windows signing conf 2024-11-23 05:27:12 -08:00
Gregory Schier
9663018e21 Update tauri.conf.json 2024-11-22 08:51:29 -08:00
Gregory Schier
80a7c2a9c7 Try updating protoc 2024-11-22 07:09:59 -08:00
Gregory Schier
4687723176 Fix Tauri signing account name 2024-11-22 06:17:37 -08:00
Gregory Schier
41ce2df00c Move signing into Tauri 2024-11-22 05:49:24 -08:00
Gregory Schier
794967904a Try signing again 2024-11-21 13:43:05 -08:00
Gregory Schier
74a7a1a21a Merge remote-tracking branch 'origin/master' 2024-11-21 13:13:10 -08:00
Gregory Schier
d9587aa314 First attempt at Windows signing 2024-11-21 13:13:05 -08:00
dependabot[bot]
6b208ef67c Bump cross-spawn (#135)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-20 21:15:30 -08:00
Gregory Schier
0cfec0ada6 Fix GraphQL body handling 2024-11-18 07:17:19 -08:00
Gregory Schier
3ecfb15c89 Fix lint 2024-11-16 15:31:14 -08:00
Gregory Schier
23c026126f Tweak schema menu 2024-11-16 15:17:26 -08:00
Gregory Schier
ff9abab547 More control over GraphQL introspection 2024-11-16 14:27:13 -08:00
Gregory Schier
c9c48c77e4 Update Tauri deps 2024-11-16 13:56:32 -08:00
Hao Xiang
83efc58f29 don't lost request's name and folder when updated by curl (#131) 2024-11-14 14:03:22 -08:00
Gregory Schier
632e1ff091 Update README.md 2024-11-12 19:31:55 -08:00
Gregory Schier
40286756b9 Update README.md 2024-11-11 07:48:39 -08:00
Hao Xiang
1050ac5e3c fix(grpc): proto dep topo order to solve panic (#130) 2024-10-29 14:19:11 -07:00
Gregory Schier
6d2c3712c0 Fix active cookie jar and improve routing 2024-10-28 10:06:43 -07:00
Gregory Schier
4a52095033 Better template function fetching 2024-10-24 08:17:58 -07:00
Gregory Schier
55b12d7329 Try fix for template tags not re-fetching on Windows 2024-10-24 07:47:20 -07:00
Gregory Schier
f4240e5229 Prevent bg flash on context menu in sidebar 2024-10-23 10:07:31 -07:00
Gregory Schier
7759649963 Update local model stores in all mutations (#129) 2024-10-23 09:54:43 -07:00
Gregory Schier
c5e6d6f2cb Some tweaks to request deletion 2024-10-23 06:27:38 -07:00
Gregory Schier
ec850f2cf0 Properly handle charset in content-type 2024-10-23 05:49:14 -07:00
Gregory Schier
ff52ad5345 Handle quotes around charset 2024-10-23 05:44:37 -07:00
Gregory Schier
5de50c70c6 Fix workspace/request creation race 2024-10-22 14:27:12 -07:00
Gregory Schier
94f8949ca2 Fix formatter < 0 error 2024-10-22 14:26:45 -07:00
Gregory Schier
44fc3c8d2d Add formatter test for escaped characters 2024-10-22 08:11:03 -07:00
Gregory Schier
57a05d5486 Fix up some of the new formatting stuff 2024-10-22 08:07:56 -07:00
Gregory Schier
e216214085 Custom JSON formatter that works with template syntax (#128) 2024-10-21 15:17:14 -07:00
Gregory Schier
aa7f18a16f Order cmd+k results by match score 2024-10-21 12:54:15 -07:00
Gregory Schier
b9f397e04a Fix response filtering 2024-10-21 07:26:50 -07:00
Gregory Schier
57c3a86799 Animate up instead of down when dropdowns open up 2024-10-18 11:22:05 -07:00
Gregory Schier
52ac41b0c6 Move elapsed calculation 2024-10-18 10:53:04 -07:00
Gregory Schier
741ccbe741 Add labels to plugin event subscribers 2024-10-18 10:46:30 -07:00
Gregory Schier
2ecd86da78 Update README.md 2024-10-18 08:27:24 -07:00
Gregory Schier
30e4e7665a Remove ios config 2024-10-18 07:59:28 -07:00
Gregory Schier
516dfd1f19 Fix GraphQL introspection 2024-10-18 06:57:44 -07:00
Gregory Schier
0cd08499aa Render sending gRPC events 2024-10-17 12:03:35 -07:00
Gregory Schier
c652df82a3 Fix SSE event selection 2024-10-17 11:28:10 -07:00
Gregory Schier
c8342fb0a9 Delete send history for workspace 2024-10-17 11:17:27 -07:00
Gregory Schier
d0b59a0fb4 Show folder structure in request selection 2024-10-17 10:53:48 -07:00
Gregory Schier
6f50f35519 Bump Tauri to fix macOS 13 launch issue 2024-10-15 09:54:21 -07:00
Gregory Schier
4e775b2b49 Undo minimumSystemVersion 2024-10-15 07:49:27 -07:00
Gregory Schier
e77a9e5d44 Rebuild plugins 2024-10-15 07:48:26 -07:00
Gregory Schier
a381e44d8c Prevent stale content flash after editing request name 2024-10-15 07:32:00 -07:00
Gregory Schier
4acf0969e8 Only sync models from active workspace 2024-10-15 07:31:42 -07:00
Gregory Schier
30c4178269 Disable autocomplete/correct/etc in plain input 2024-10-14 21:46:48 -07:00
Gregory Schier
dffe6e0a16 Intelligent readonly editor updates, to preserve scroll 2024-10-14 10:40:09 -07:00
91 changed files with 2085 additions and 1134 deletions

View File

@@ -22,7 +22,7 @@ jobs:
- platform: 'macos-latest' # for Intel-based Macs.
args: '--target x86_64-apple-darwin'
yaak_arch: 'x64'
- platform: 'ubuntu-22.04' # for Tauri v1, you could replace this with ubuntu-20.04.
- platform: 'ubuntu-22.04'
args: ''
yaak_arch: 'x64'
- platform: 'windows-latest'
@@ -38,10 +38,6 @@ jobs:
with:
node-version: 22
- uses: actions/setup-go@v5
with:
go-version: '1.22'
- name: install Rust stable
uses: dtolnay/rust-toolchain@stable
with:
@@ -56,7 +52,7 @@ jobs:
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
src-tauri/target/
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
restore-keys: ${{ runner.os }}-cargo-
@@ -66,6 +62,10 @@ jobs:
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
- name: install dependencies (windows only)
if: matrix.platform == 'windows-latest'
run: cargo install trusted-signing-cli
- name: Install NPM Dependencies
run: |
npm ci
@@ -94,16 +94,24 @@ jobs:
env:
YAAK_PLUGINS_DIR: ${{ env.YAAK_PLUGINS_DIR }}
YAAK_TARGET_ARCH: ${{ matrix.yaak_arch }}
ENABLE_CODE_SIGNING: ${{ secrets.APPLE_CERTIFICATE }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
ENABLE_CODE_SIGNING: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
# Apple signing stuff
APPLE_CERTIFICATE: ${{ matrix.platform == 'macos-latest' && secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ matrix.platform == 'macos-latest' && secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_ID: ${{ matrix.platform == 'macos-latest' && secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ matrix.platform == 'macos-latest' && secrets.APPLE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ matrix.platform == 'macos-latest' && secrets.APPLE_SIGNING_IDENTITY }}
APPLE_TEAM_ID: ${{ matrix.platform == 'macos-latest' && secrets.APPLE_TEAM_ID }}
# Windows signing stuff
AZURE_CLIENT_ID: ${{ matrix.platform == 'windows-latest' && secrets.AZURE_CLIENT_ID }}
AZURE_CLIENT_SECRET: ${{ matrix.platform == 'windows-latest' && secrets.AZURE_CLIENT_SECRET }}
AZURE_TENANT_ID: ${{ matrix.platform == 'windows-latest' && secrets.AZURE_TENANT_ID }}
with:
tagName: 'v__VERSION__'
releaseName: 'Release __VERSION__'

View File

@@ -1,4 +1,4 @@
# [Yaak API Client](https://yaak.app)
# Yaak API Client
Yaak is a desktop API client for organizing and executing REST, GraphQL, and gRPC
requests. It's built using [Tauri](https://tauri.app), Rust, and ReactJS.

143
package-lock.json generated
View File

@@ -18,7 +18,7 @@
"src-web"
],
"devDependencies": {
"@tauri-apps/cli": "^2.0.2",
"@tauri-apps/cli": "^2.1.0",
"@typescript-eslint/eslint-plugin": "^8.5.0",
"@typescript-eslint/parser": "^8.5.0",
"eslint": "^8",
@@ -2597,9 +2597,9 @@
}
},
"node_modules/@tanstack/query-core": {
"version": "5.59.6",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.59.6.tgz",
"integrity": "sha512-g58YTHe4ClRrjJ50GY9fas/0zARJVozY0Hs+hcSBOmwZaeKY+to0/LX8wKnnH/EJiLYcC1sHmE11CAS3ncfZBg==",
"version": "5.59.16",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.59.16.tgz",
"integrity": "sha512-crHn+G3ltqb5JG0oUv6q+PMz1m1YkjpASrXTU+sYWW9pLk0t2GybUHNRqYPZWhxgjPaVGC4yp92gSFEJgYEsPw==",
"license": "MIT",
"funding": {
"type": "github",
@@ -2618,12 +2618,12 @@
}
},
"node_modules/@tanstack/react-query": {
"version": "5.59.6",
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.59.6.tgz",
"integrity": "sha512-sGg2sNKg8cYf6aS1dzDf4weN+Vt9PfUu+0btwerrbtYysdNBbcGD4rPe9jhPgMtpDDlvi4cbLv+j1Qo814Kf+Q==",
"version": "5.59.16",
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.59.16.tgz",
"integrity": "sha512-MuyWheG47h6ERd4PKQ6V8gDyBu3ThNG22e1fRVwvq6ap3EqsFhyuxCAwhNP/03m/mLg+DAb0upgbPaX6VB+CkQ==",
"license": "MIT",
"dependencies": {
"@tanstack/query-core": "5.59.6"
"@tanstack/query-core": "5.59.16"
},
"funding": {
"type": "github",
@@ -2689,9 +2689,9 @@
}
},
"node_modules/@tauri-apps/cli": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.0.2.tgz",
"integrity": "sha512-R4ontHZvXORArERAHIidp5zRfZEshZczTiK+poslBv7AGKpQZoMw+E49zns7mOmP64i2Cq9Ci0pJvi4Rm8Okzw==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.1.0.tgz",
"integrity": "sha512-K2VhcKqBhAeS5pNOVdnR/xQRU6jwpgmkSL2ejHXcl0m+kaTggT0WRDQnFtPq6NljA7aE03cvwsbCAoFG7vtkJw==",
"dev": true,
"license": "Apache-2.0 OR MIT",
"bin": {
@@ -2705,22 +2705,22 @@
"url": "https://opencollective.com/tauri"
},
"optionalDependencies": {
"@tauri-apps/cli-darwin-arm64": "2.0.2",
"@tauri-apps/cli-darwin-x64": "2.0.2",
"@tauri-apps/cli-linux-arm-gnueabihf": "2.0.2",
"@tauri-apps/cli-linux-arm64-gnu": "2.0.2",
"@tauri-apps/cli-linux-arm64-musl": "2.0.2",
"@tauri-apps/cli-linux-x64-gnu": "2.0.2",
"@tauri-apps/cli-linux-x64-musl": "2.0.2",
"@tauri-apps/cli-win32-arm64-msvc": "2.0.2",
"@tauri-apps/cli-win32-ia32-msvc": "2.0.2",
"@tauri-apps/cli-win32-x64-msvc": "2.0.2"
"@tauri-apps/cli-darwin-arm64": "2.1.0",
"@tauri-apps/cli-darwin-x64": "2.1.0",
"@tauri-apps/cli-linux-arm-gnueabihf": "2.1.0",
"@tauri-apps/cli-linux-arm64-gnu": "2.1.0",
"@tauri-apps/cli-linux-arm64-musl": "2.1.0",
"@tauri-apps/cli-linux-x64-gnu": "2.1.0",
"@tauri-apps/cli-linux-x64-musl": "2.1.0",
"@tauri-apps/cli-win32-arm64-msvc": "2.1.0",
"@tauri-apps/cli-win32-ia32-msvc": "2.1.0",
"@tauri-apps/cli-win32-x64-msvc": "2.1.0"
}
},
"node_modules/@tauri-apps/cli-darwin-arm64": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.0.2.tgz",
"integrity": "sha512-B+/a8Q6wAqmB4A4HVeK0oQP5TdQGKW60ZLOI9O2ktH2HPr9ETr3XkwXPuJ2uAOuGEgtRZHBgFOIgG000vMnKlg==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.1.0.tgz",
"integrity": "sha512-ESc6J6CE8hl1yKH2vJ+ALF+thq4Be+DM1mvmTyUCQObvezNCNhzfS6abIUd3ou4x5RGH51ouiANeT3wekU6dCw==",
"cpu": [
"arm64"
],
@@ -2735,9 +2735,9 @@
}
},
"node_modules/@tauri-apps/cli-darwin-x64": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.0.2.tgz",
"integrity": "sha512-kaurhn6XT4gAVCPAQSSHl/CHFxTS0ljc47N7iGTSlYJ03sCWPRZeNuVa/bn6rolz9MA2JfnRnFqB1pUL6jzp9Q==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.1.0.tgz",
"integrity": "sha512-TasHS442DFs8cSH2eUQzuDBXUST4ECjCd0yyP+zZzvAruiB0Bg+c8A+I/EnqCvBQ2G2yvWLYG8q/LI7c87A5UA==",
"cpu": [
"x64"
],
@@ -2752,9 +2752,9 @@
}
},
"node_modules/@tauri-apps/cli-linux-arm-gnueabihf": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.0.2.tgz",
"integrity": "sha512-bVrofjlacMxmGMcqK18iBW05tsZXOd19/MnqruFFcHSVjvkGGIXHMtUbMXnZNXBPkHDsnfytNtkY9SZGfCFaBA==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.1.0.tgz",
"integrity": "sha512-aP7ZBGNL4ny07Cbb6kKpUOSrmhcIK2KhjviTzYlh+pPhAptxnC78xQGD3zKQkTi2WliJLPmBYbOHWWQa57lQ9w==",
"cpu": [
"arm"
],
@@ -2769,9 +2769,9 @@
}
},
"node_modules/@tauri-apps/cli-linux-arm64-gnu": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.0.2.tgz",
"integrity": "sha512-7XCBn0TTBVQGnV42dXcbHPLg/9W8kJoVzuliIozvNGyRWxfXqDbQYzpI48HUQG3LgHMabcw8+pVZAfGhevLrCA==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.1.0.tgz",
"integrity": "sha512-ZTdgD5gLeMCzndMT2f358EkoYkZ5T+Qy6zPzU+l5vv5M7dHVN9ZmblNAYYXmoOuw7y+BY4X/rZvHV9pcGrcanQ==",
"cpu": [
"arm64"
],
@@ -2786,9 +2786,9 @@
}
},
"node_modules/@tauri-apps/cli-linux-arm64-musl": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.0.2.tgz",
"integrity": "sha512-1xi2SreGVlpAL68MCsDUY63rdItUdPZreXIAcOVqvUehcJRYOa1XGSBhrV0YXRgZeh0AtKC19z6PRzcv4rosZA==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.1.0.tgz",
"integrity": "sha512-NzwqjUCilhnhJzusz3d/0i0F1GFrwCQbkwR6yAHUxItESbsGYkZRJk0yMEWkg3PzFnyK4cWTlQJMEU52TjhEzA==",
"cpu": [
"arm64"
],
@@ -2803,9 +2803,9 @@
}
},
"node_modules/@tauri-apps/cli-linux-x64-gnu": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.0.2.tgz",
"integrity": "sha512-WVjwYzPWFqZVg1fx6KSU5w47Q0VbMyaCp34qs5EcS8EIU0/RnofdzqUoOYqvgGVgNgoz7Pj5dXK2SkS8BHXMmA==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.1.0.tgz",
"integrity": "sha512-TyiIpMEtZxNOQmuFyfJwaaYbg3movSthpBJLIdPlKxSAB2BW0VWLY3/ZfIxm/G2YGHyREkjJvimzYE0i37PnMA==",
"cpu": [
"x64"
],
@@ -2820,9 +2820,9 @@
}
},
"node_modules/@tauri-apps/cli-linux-x64-musl": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.0.2.tgz",
"integrity": "sha512-h5miE2mctgaQNn/BbG9o1pnJcrx+VGBi2A6JFqGu934lFgSV5+s28M8Gc8AF2JgFH4hQV4IuMkeSw8Chu5Dodg==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.1.0.tgz",
"integrity": "sha512-/dQd0TlaxBdJACrR72DhynWftzHDaX32eBtS5WBrNJ+nnNb+znM3gON6nJ9tSE9jgDa6n1v2BkI/oIDtypfUXw==",
"cpu": [
"x64"
],
@@ -2837,9 +2837,9 @@
}
},
"node_modules/@tauri-apps/cli-win32-arm64-msvc": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.0.2.tgz",
"integrity": "sha512-2b8oO0+dYonahG5PfA/zoq0zlafLclfmXgqoWDZ++UiPtQHJNpNeEQ8GWbSFKGHQ494Jo6jHvazOojGRE1kqAg==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.1.0.tgz",
"integrity": "sha512-NdQJO7SmdYqOcE+JPU7bwg7+odfZMWO6g8xF9SXYCMdUzvM2Gv/AQfikNXz5yS7ralRhNFuW32i5dcHlxh4pDg==",
"cpu": [
"arm64"
],
@@ -2854,9 +2854,9 @@
}
},
"node_modules/@tauri-apps/cli-win32-ia32-msvc": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.0.2.tgz",
"integrity": "sha512-axgICLunFi0To3EibdCBgbST5RocsSmtM4c04+CbcX8WQQosJ9ziWlCSrrOTRr+gJERAMSvEyVUS98f6bWMw9A==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.1.0.tgz",
"integrity": "sha512-f5h8gKT/cB8s1ticFRUpNmHqkmaLutT62oFDB7N//2YTXnxst7EpMIn1w+QimxTvTk2gcx6EcW6bEk/y2hZGzg==",
"cpu": [
"ia32"
],
@@ -2871,9 +2871,9 @@
}
},
"node_modules/@tauri-apps/cli-win32-x64-msvc": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.0.2.tgz",
"integrity": "sha512-JR17cM6+DyExZRgpXr2/DdqvcFYi/EKvQt8dI5R1/uQoesWd8jeNnrU7c1FG1Zmw9+pTzDztsNqEKsrNq2sNIg==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.1.0.tgz",
"integrity": "sha512-P/+LrdSSb5Xbho1LRP4haBjFHdyPdjWvGgeopL96OVtrFpYnfC+RctB45z2V2XxqFk3HweDDxk266btjttfjGw==",
"cpu": [
"x64"
],
@@ -3127,6 +3127,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/whatwg-mimetype": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz",
"integrity": "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/yauzl": {
"version": "2.10.3",
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
@@ -4922,10 +4929,9 @@
"license": "MIT"
},
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
"license": "MIT",
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
@@ -9218,11 +9224,10 @@
"license": "MIT"
},
"node_modules/npm-run-all/node_modules/cross-spawn": {
"version": "6.0.5",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
"integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==",
"version": "6.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz",
"integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==",
"dev": true,
"license": "MIT",
"dependencies": {
"nice-try": "^1.0.4",
"path-key": "^2.0.1",
@@ -10482,11 +10487,10 @@
}
},
"node_modules/react-devtools/node_modules/execa/node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
@@ -13242,6 +13246,15 @@
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"license": "BSD-2-Clause"
},
"node_modules/whatwg-mimetype": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
"integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
@@ -13751,7 +13764,7 @@
"@lezer/lr": "^1.3.3",
"@react-hook/resize-observer": "^2.0.2",
"@tailwindcss/container-queries": "^0.1.1",
"@tanstack/react-query": "^5.55.4",
"@tanstack/react-query": "^5.59.16",
"@tanstack/react-virtual": "^3.10.8",
"@tauri-apps/api": "^2.0.1",
"@tauri-apps/plugin-clipboard-manager": "^2.0.0",
@@ -13786,6 +13799,7 @@
"react-use": "^17.5.1",
"slugify": "^1.6.6",
"uuid": "^10.0.0",
"whatwg-mimetype": "^4.0.0",
"xml-formatter": "^3.6.3"
},
"devDependencies": {
@@ -13799,6 +13813,7 @@
"@types/react": "^18.3.5",
"@types/react-dom": "^18.3.0",
"@types/uuid": "^10.0.0",
"@types/whatwg-mimetype": "^3.0.2",
"@vitejs/plugin-react": "^4.3.1",
"autoprefixer": "^10.4.20",
"decompress": "^4.2.1",

View File

@@ -31,7 +31,7 @@
"tauri-before-dev": "npm run --workspaces --if-present dev"
},
"devDependencies": {
"@tauri-apps/cli": "^2.0.2",
"@tauri-apps/cli": "^2.1.0",
"@typescript-eslint/eslint-plugin": "^8.5.0",
"@typescript-eslint/parser": "^8.5.0",
"eslint": "^8",

View File

@@ -1,10 +1,10 @@
const decompress = require('decompress');
const Downloader = require('nodejs-file-downloader');
const path = require('node:path');
const { rmSync, mkdirSync, cpSync, existsSync } = require('node:fs');
const { rmSync, mkdirSync, cpSync, existsSync, statSync, chmodSync } = require('node:fs');
const { execSync } = require('node:child_process');
const VERSION = '27.2';
const VERSION = '28.3';
// `${process.platform}_${process.arch}`
const MAC_ARM = 'darwin_arm64';
@@ -67,6 +67,11 @@ mkdirSync(dstDir, { recursive: true });
cpSync(includeSrc, includeDst, { recursive: true });
rmSync(tmpDir, { recursive: true, force: true });
// Make binary writable, so we can sign it during release
const stat = statSync(binDst);
const newMode = stat.mode | 0o200;
chmodSync(binDst, newMode);
console.log('Downloaded protoc to', binDst);
})().catch((err) => console.log('Script failed:', err));

335
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -16,7 +16,7 @@ crate-type = ["staticlib", "cdylib", "lib"]
strip = true # Automatically strip symbols from the binary.
[build-dependencies]
tauri-build = { version = "2.0.1", features = [] }
tauri-build = { version = "2.0.3", features = [] }
[target.'cfg(target_os = "macos")'.dependencies]
objc = "0.2.7"
@@ -29,9 +29,8 @@ openssl-sys = { version = "0.9", features = ["vendored"] } # For Ubuntu installa
yaak_grpc = { path = "yaak_grpc" }
yaak_templates = { path = "yaak_templates" }
yaak_plugin_runtime = { workspace = true }
yaak_sse = { workspace = true }
yaak_models = { workspace = true }
yaak_sse = { path = "yaak_sse" }
anyhow = "1.0.86"
base64 = "0.22.0"
chrono = { version = "0.4.31", features = ["serde"] }
datetime = "0.5.2"
@@ -42,14 +41,13 @@ rand = "0.8.5"
regex = "1.10.2"
reqwest = { version = "0.12.4", features = ["multipart", "cookies", "gzip", "brotli", "deflate", "json", "native-tls-alpn"] }
reqwest_cookie_store = "0.8.0"
serde = { version = "1.0.198", features = ["derive"] }
serde_json = { version = "1.0.116", features = ["raw_value"] }
serde_yaml = "0.9.34"
tauri = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true, features = ["raw_value"] }
tauri = { workspace = true, features = ["devtools", "protocol-asset"] }
tauri-plugin-shell = { workspace = true }
tauri-plugin-clipboard-manager = "2.0.1"
tauri-plugin-dialog = "2.0.1"
tauri-plugin-fs = "2.0.1"
tauri-plugin-dialog = "2.0.3"
tauri-plugin-fs = "2.0.3"
tauri-plugin-log = { version = "2.0.1", features = ["colored"] }
tauri-plugin-os = "2.0.1"
tauri-plugin-updater = "2.0.2"
@@ -57,13 +55,17 @@ tauri-plugin-window-state = "2.0.1"
tokio = { version = "1.36.0", features = ["sync"] }
tokio-stream = "0.1.15"
uuid = "1.7.0"
thiserror = "1.0.61"
mime_guess = "2.0.5"
urlencoding = "2.1.3"
eventsource-client = { git = "https://github.com/yaakapp/rust-eventsource-client", version = "0.13.0" }
[workspace.dependencies]
yaak_models = { path = "yaak_models" }
yaak_sse = { path = "yaak_sse" }
yaak_plugin_runtime = { path = "yaak_plugin_runtime" }
tauri-plugin-shell = "2.0.1"
tauri = { version = "2.0.3", features = ["devtools", "protocol-asset"] }
serde = "1.0.215"
serde_json = "1.0.132"
tauri-plugin-shell = "2.0.2"
tauri = { version = "2.1.1", features = ["devtools", "protocol-asset"] }
thiserror = "2.0.3"
ts-rs = "10.0.0"

View File

@@ -37,7 +37,7 @@
],
"definitions": {
"Capability": {
"description": "A grouping and boundary mechanism developers can use to isolate access to the IPC layer.\n\nIt controls application windows fine grained access to the Tauri core, application, or plugin commands. If a window is not matching any capability then it has no access to the IPC layer at all.\n\nThis can be done to create groups of windows, based on their required system access, which can reduce impact of frontend vulnerabilities in less privileged windows. Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`. A Window can have none, one, or multiple associated capabilities.\n\n## Example\n\n```json { \"identifier\": \"main-user-files-write\", \"description\": \"This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programatic access to files selected by the user.\", \"windows\": [ \"main\" ], \"permissions\": [ \"core:default\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] }, \"platforms\": [\"macOS\",\"windows\"] } ```",
"description": "A grouping and boundary mechanism developers can use to isolate access to the IPC layer.\n\nIt controls application windows fine grained access to the Tauri core, application, or plugin commands. If a window is not matching any capability then it has no access to the IPC layer at all.\n\nThis can be done to create groups of windows, based on their required system access, which can reduce impact of frontend vulnerabilities in less privileged windows. Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`. A Window can have none, one, or multiple associated capabilities.\n\n## Example\n\n```json { \"identifier\": \"main-user-files-write\", \"description\": \"This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programatic access to files selected by the user.\", \"windows\": [ \"main\" ], \"permissions\": [ \"core:default\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] }, ], \"platforms\": [\"macOS\",\"windows\"] } ```",
"type": "object",
"required": [
"identifier",
@@ -84,7 +84,7 @@
}
},
"permissions": {
"description": "List of permissions attached to this capability.\n\nMust include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`. For commands directly implemented in the application itself only `${permission-name}` is required.\n\n## Example\n\n```json [ \"core:default\", \"shell:allow-open\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] } ```",
"description": "List of permissions attached to this capability.\n\nMust include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`. For commands directly implemented in the application itself only `${permission-name}` is required.\n\n## Example\n\n```json [ \"core:default\", \"shell:allow-open\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] } ] ```",
"type": "array",
"items": {
"$ref": "#/definitions/PermissionEntry"

View File

@@ -37,7 +37,7 @@
],
"definitions": {
"Capability": {
"description": "A grouping and boundary mechanism developers can use to isolate access to the IPC layer.\n\nIt controls application windows fine grained access to the Tauri core, application, or plugin commands. If a window is not matching any capability then it has no access to the IPC layer at all.\n\nThis can be done to create groups of windows, based on their required system access, which can reduce impact of frontend vulnerabilities in less privileged windows. Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`. A Window can have none, one, or multiple associated capabilities.\n\n## Example\n\n```json { \"identifier\": \"main-user-files-write\", \"description\": \"This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programatic access to files selected by the user.\", \"windows\": [ \"main\" ], \"permissions\": [ \"core:default\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] }, \"platforms\": [\"macOS\",\"windows\"] } ```",
"description": "A grouping and boundary mechanism developers can use to isolate access to the IPC layer.\n\nIt controls application windows fine grained access to the Tauri core, application, or plugin commands. If a window is not matching any capability then it has no access to the IPC layer at all.\n\nThis can be done to create groups of windows, based on their required system access, which can reduce impact of frontend vulnerabilities in less privileged windows. Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`. A Window can have none, one, or multiple associated capabilities.\n\n## Example\n\n```json { \"identifier\": \"main-user-files-write\", \"description\": \"This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programatic access to files selected by the user.\", \"windows\": [ \"main\" ], \"permissions\": [ \"core:default\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] }, ], \"platforms\": [\"macOS\",\"windows\"] } ```",
"type": "object",
"required": [
"identifier",
@@ -84,7 +84,7 @@
}
},
"permissions": {
"description": "List of permissions attached to this capability.\n\nMust include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`. For commands directly implemented in the application itself only `${permission-name}` is required.\n\n## Example\n\n```json [ \"core:default\", \"shell:allow-open\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] } ```",
"description": "List of permissions attached to this capability.\n\nMust include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`. For commands directly implemented in the application itself only `${permission-name}` is required.\n\n## Example\n\n```json [ \"core:default\", \"shell:allow-open\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] } ] ```",
"type": "array",
"items": {
"$ref": "#/definitions/PermissionEntry"

View File

@@ -243,7 +243,7 @@ pub async fn send_http_request<R: Runtime>(
let request_body = rendered_request.body;
if let Some(body_type) = &rendered_request.body_type {
if request_body.contains_key("query") && request_body.contains_key("variables") {
if body_type == "graphql" {
let query = get_str_h(&request_body, "query");
let variables = get_str_h(&request_body, "variables");
let body = if variables.trim().is_empty() {
@@ -255,9 +255,6 @@ pub async fn send_http_request<R: Runtime>(
)
};
request_builder = request_builder.body(body.to_owned());
} else if request_body.contains_key("text") {
let body = get_str_h(&request_body, "text");
request_builder = request_builder.body(body.to_owned());
} else if body_type == "application/x-www-form-urlencoded"
&& request_body.contains_key("form")
{
@@ -359,6 +356,9 @@ pub async fn send_http_request<R: Runtime>(
}
headers.remove("Content-Type"); // reqwest will add this automatically
request_builder = request_builder.multipart(multipart_form);
} else if request_body.contains_key("text") {
let body = get_str_h(&request_body, "text");
request_builder = request_builder.body(body.to_owned());
} else {
warn!("Unsupported body type: {}", body_type);
}
@@ -406,9 +406,9 @@ pub async fn send_http_request<R: Runtime>(
let base_dir = dir.join("responses");
create_dir_all(base_dir.clone()).await.expect("Failed to create responses dir");
let body_path = if response_id.is_empty() {
base_dir.join(response_id.clone())
} else {
base_dir.join(uuid::Uuid::new_v4().to_string())
} else {
base_dir.join(response_id.clone())
};
{
@@ -459,11 +459,11 @@ pub async fn send_http_request<R: Runtime>(
}
match chunk {
Ok(Some(bytes)) => {
let mut r = response.lock().await;
r.elapsed = start.elapsed().as_millis() as i32;
f.write_all(&bytes).await.expect("Failed to write to file");
f.flush().await.expect("Failed to flush file");
written_bytes += bytes.len();
let mut r = response.lock().await;
r.elapsed = start.elapsed().as_millis() as i32;
r.content_length = Some(written_bytes as i32);
update_response_if_id(&window, &r)
.await

File diff suppressed because it is too large Load Diff

View File

@@ -45,8 +45,8 @@
"active": true,
"category": "DeveloperTool",
"externalBin": [
"vendored/protoc/yaakprotoc",
"vendored/node/yaaknode"
"vendored/node/yaaknode",
"vendored/protoc/yaakprotoc"
],
"icon": [
"icons/release/32x32.png",
@@ -72,9 +72,6 @@
"rpm"
],
"createUpdaterArtifacts": "v1Compatible",
"iOS": {
"developmentTeam": "7PU3P6ELJ8"
},
"macOS": {
"minimumSystemVersion": "13.0",
"exceptionDomain": "",
@@ -82,8 +79,7 @@
"frameworks": []
},
"windows": {
"digestAlgorithm": "sha256",
"timestampUrl": ""
"signCommand": "trusted-signing-cli -e https://eus.codesigning.azure.net/ -a Yaak -c yaakapp %1"
}
}
}

View File

@@ -63,8 +63,12 @@ async function pluginHookExport(_ctx, request) {
}
xs.push(NEWLINE);
}
} else if (typeof request.body?.query === "string") {
const body = { query: request.body.query || "", variables: maybeParseJSON(request.body.variables, void 0) };
xs.push("--data-raw", `${quote(JSON.stringify(body))}`);
xs.push(NEWLINE);
} else if (typeof request.body?.text === "string") {
xs.push("--data-raw", `$${quote(request.body.text)}`);
xs.push("--data-raw", `${quote(request.body.text)}`);
xs.push(NEWLINE);
}
if (request.authenticationType === "basic" || request.authenticationType === "digest") {
@@ -91,6 +95,13 @@ function quote(arg) {
function onlyEnabled(v) {
return v.enabled !== false && !!v.name;
}
function maybeParseJSON(v, fallback) {
try {
return JSON.parse(v);
} catch (err) {
return fallback;
}
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
plugin,

View File

@@ -288,7 +288,8 @@ var SUPPORTED_ARGS = [
// Request method
DATA_FLAGS
].flatMap((v) => v);
function pluginHookImport(ctx, rawData) {
var BOOL_FLAGS = ["G", "get", "digest"];
function pluginHookImport(_ctx, rawData) {
if (!rawData.match(/^\s*curl /)) {
return null;
}
@@ -359,10 +360,11 @@ function importCommand(parseEntries, workspaceId) {
}
let value;
const nextEntry = parseEntries[i + 1];
const hasValue = !BOOL_FLAGS.includes(name);
if (isSingleDash && name.length > 1) {
value = name.slice(1);
name = name.slice(0, 1);
} else if (typeof nextEntry === "string" && !nextEntry.startsWith("-")) {
} else if (typeof nextEntry === "string" && hasValue && !nextEntry.startsWith("-")) {
value = nextEntry;
i++;
} else {

View File

@@ -0,0 +1,55 @@
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var src_exports = {};
__export(src_exports, {
plugin: () => plugin
});
module.exports = __toCommonJS(src_exports);
var import_node_fs = __toESM(require("node:fs"));
var plugin = {
templateFunctions: [{
name: "fs.readFile",
description: "Read the contents of a file as utf-8",
args: [{ title: "Select File", type: "file", name: "path", label: "File" }],
async onRender(_ctx, args) {
if (!args.values.path) return null;
try {
return import_node_fs.default.promises.readFile(args.values.path, "utf-8");
} catch (err) {
return null;
}
}
}]
};
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
plugin
});

View File

@@ -0,0 +1,9 @@
{
"name": "@yaakapp/template-function-fs",
"private": true,
"version": "0.0.1",
"scripts": {
"build": "yaakcli build ./src/index.ts",
"dev": "yaakcli dev ./src/index.js"
}
}

View File

@@ -28,6 +28,7 @@ var algorithms = ["md5", "sha1", "sha256", "sha512"];
var plugin = {
templateFunctions: algorithms.map((algorithm) => ({
name: `hash.${algorithm}`,
description: "Hash a value to its hexidecimal representation",
args: [
{
name: "input",

View File

@@ -26,24 +26,21 @@ module.exports = __toCommonJS(src_exports);
var plugin = {
templateFunctions: [{
name: "prompt.text",
description: "Prompt the user for input when sending a request",
args: [
{ type: "text", name: "title", label: "Title" },
{ type: "text", name: "label", label: "Label", optional: true },
{ type: "text", name: "defaultValue", label: "Default Value", optional: true },
{ type: "text", name: "placeholder", label: "Placeholder", optional: true }
],
async onRender(ctx, args) {
console.log("PROMPT", args);
if (args.purpose !== "send") return null;
const value = await ctx.prompt.text({
return await ctx.prompt.text({
id: `prompt-${args.values.label}`,
label: args.values.label ?? "",
label: args.values.title ?? "",
title: args.values.title ?? "",
defaultValue: args.values.defaultValue,
placeholder: args.values.placeholder
});
console.log("VALUE", value);
return value;
}
}]
};

View File

@@ -8837,6 +8837,7 @@ var plugin = {
templateFunctions: [
{
name: "response.header",
description: "Read the value of a response header, by name",
args: [
requestArg,
{
@@ -8863,6 +8864,7 @@ var plugin = {
},
{
name: "response.body.path",
description: "Access a field of the response body using JsonPath or XPath",
aliases: ["response"],
args: [
requestArg,
@@ -8901,6 +8903,34 @@ var plugin = {
}
return null;
}
},
{
name: "response.body.raw",
description: "Access the entire response body, as text",
aliases: ["response"],
args: [
requestArg,
behaviorArg
],
async onRender(ctx, args) {
if (!args.values.request) return null;
const response = await getResponse(ctx, {
requestId: args.values.request,
purpose: args.purpose,
behavior: args.values.behavior ?? null
});
if (response == null) return null;
if (response.bodyPath == null) {
return null;
}
let body;
try {
body = (0, import_node_fs.readFileSync)(response.bodyPath, "utf-8");
} catch (_) {
return null;
}
return body;
}
}
]
};

View File

@@ -22,3 +22,4 @@ tauri = { workspace = true }
tauri-plugin-shell = { workspace = true }
md5 = "0.7.0"
dunce = "1.0.4"
async-recursion = "1.1.1"

View File

@@ -4,6 +4,7 @@ use std::path::PathBuf;
use std::str::FromStr;
use anyhow::anyhow;
use async_recursion::async_recursion;
use hyper::client::HttpConnector;
use hyper::Client;
use hyper_rustls::{HttpsConnector, HttpsConnectorBuilder};
@@ -38,9 +39,8 @@ pub async fn fill_pool_from_files(
.expect("failed to resolve protoc include directory");
// HACK: Remove UNC prefix for Windows paths
let global_import_dir = dunce::simplified(global_import_dir.as_path())
.to_string_lossy()
.to_string();
let global_import_dir =
dunce::simplified(global_import_dir.as_path()).to_string_lossy().to_string();
let desc_path = dunce::simplified(desc_path.as_path());
let mut args = vec![
@@ -89,12 +89,9 @@ pub async fn fill_pool_from_files(
let bytes = fs::read(desc_path).await.map_err(|e| e.to_string())?;
let fdp = FileDescriptorSet::decode(bytes.deref()).map_err(|e| e.to_string())?;
pool.add_file_descriptor_set(fdp)
.map_err(|e| e.to_string())?;
pool.add_file_descriptor_set(fdp).map_err(|e| e.to_string())?;
fs::remove_file(desc_path)
.await
.map_err(|e| e.to_string())?;
fs::remove_file(desc_path).await.map_err(|e| e.to_string())?;
Ok(pool)
}
@@ -107,6 +104,10 @@ pub async fn fill_pool_from_reflection(uri: &Uri) -> Result<DescriptorPool, Stri
if service == "grpc.reflection.v1alpha.ServerReflection" {
continue;
}
if service == "grpc.reflection.v1.ServerReflection"{
// TODO: update reflection client to use v1
continue;
}
file_descriptor_set_from_service_name(&service, &mut pool, &mut client).await;
}
@@ -120,10 +121,7 @@ pub fn get_transport() -> Client<HttpsConnector<HttpConnector>, BoxBody> {
http_connector.enforce_http(false);
http_connector
});
Client::builder()
.pool_max_idle_per_host(0)
.http2_only(true)
.build(connector)
Client::builder().pool_max_idle_per_host(0).http2_only(true).build(connector)
}
async fn list_services(
@@ -137,11 +135,7 @@ async fn list_services(
_ => panic!("Expected a ListServicesResponse variant"),
};
Ok(list_services_response
.service
.iter()
.map(|s| s.name.clone())
.collect::<Vec<_>>())
Ok(list_services_response.service.iter().map(|s| s.name.clone()).collect::<Vec<_>>())
}
async fn file_descriptor_set_from_service_name(
@@ -153,14 +147,11 @@ async fn file_descriptor_set_from_service_name(
client,
MessageRequest::FileContainingSymbol(service_name.into()),
)
.await
.await
{
Ok(resp) => resp,
Err(e) => {
warn!(
"Error fetching file descriptor for service {}: {}",
service_name, e
);
warn!("Error fetching file descriptor for service {}: {}", service_name, e);
return;
}
};
@@ -170,16 +161,37 @@ async fn file_descriptor_set_from_service_name(
_ => panic!("Expected a FileDescriptorResponse variant"),
};
for fd in file_descriptor_response.file_descriptor_proto {
add_file_descriptors_to_pool(file_descriptor_response.file_descriptor_proto, pool, client)
.await;
}
#[async_recursion]
async fn add_file_descriptors_to_pool(
fds: Vec<Vec<u8>>,
pool: &mut DescriptorPool,
client: &mut ServerReflectionClient<Client<HttpsConnector<HttpConnector>, BoxBody>>,
) {
let mut topo_sort = topology::SimpleTopoSort::new();
let mut fd_mapping = std::collections::HashMap::with_capacity(fds.len());
for fd in fds {
let fdp = FileDescriptorProto::decode(fd.deref()).unwrap();
// Add deps first or else we'll get an error
for dep_name in fdp.clone().dependency {
file_descriptor_set_by_filename(&dep_name, pool, client).await;
}
topo_sort.insert(fdp.name().to_string(), fdp.dependency.clone());
fd_mapping.insert(fdp.name().to_string(), fdp);
}
pool.add_file_descriptor_proto(fdp)
.expect("add file descriptor proto");
for node in topo_sort {
match node {
Ok(node) => {
if let Some(fdp) = fd_mapping.remove(&node) {
pool.add_file_descriptor_proto(fdp).expect("add file descriptor proto");
} else {
file_descriptor_set_by_filename(node.as_str(), pool, client).await;
}
}
Err(_) => panic!("proto file got cycle!"),
}
}
}
@@ -206,11 +218,8 @@ async fn file_descriptor_set_by_filename(
}
};
for fd in file_descriptor_response.file_descriptor_proto {
let fdp = FileDescriptorProto::decode(fd.deref()).unwrap();
pool.add_file_descriptor_proto(fdp)
.expect("add file descriptor proto");
}
add_file_descriptors_to_pool(file_descriptor_response.file_descriptor_proto, pool, client)
.await;
}
async fn send_reflection_request(
@@ -249,4 +258,137 @@ pub fn method_desc_to_path(md: &MethodDescriptor) -> PathAndQuery {
.ok_or_else(|| anyhow!("invalid method path"))
.expect("invalid method path");
PathAndQuery::from_str(&format!("/{}/{}", namespace, method_name)).expect("invalid method path")
}
}
mod topology {
use std::collections::{HashMap, HashSet};
pub struct SimpleTopoSort<T> {
out_graph: HashMap<T, HashSet<T>>,
in_graph: HashMap<T, HashSet<T>>,
}
impl<T> SimpleTopoSort<T>
where
T: Eq + std::hash::Hash + Clone,
{
pub fn new() -> Self {
SimpleTopoSort {
out_graph: HashMap::new(),
in_graph: HashMap::new(),
}
}
pub fn insert<I: IntoIterator<Item = T>>(&mut self, node: T, deps: I) {
self.out_graph.entry(node.clone()).or_insert(HashSet::new());
for dep in deps {
self.out_graph.entry(node.clone()).or_insert(HashSet::new()).insert(dep.clone());
self.in_graph.entry(dep.clone()).or_insert(HashSet::new()).insert(node.clone());
}
}
}
impl<T> IntoIterator for SimpleTopoSort<T>
where
T: Eq + std::hash::Hash + Clone,
{
type IntoIter = SimpleTopoSortIter<T>;
type Item = <SimpleTopoSortIter<T> as Iterator>::Item;
fn into_iter(self) -> Self::IntoIter {
SimpleTopoSortIter::new(self)
}
}
pub struct SimpleTopoSortIter<T> {
data: SimpleTopoSort<T>,
zero_indegree: Vec<T>,
}
impl<T> SimpleTopoSortIter<T>
where
T: Eq + std::hash::Hash + Clone,
{
pub fn new(data: SimpleTopoSort<T>) -> Self {
let mut zero_indegree = Vec::new();
for (node, _) in data.in_graph.iter() {
if !data.out_graph.contains_key(node) {
zero_indegree.push(node.clone());
}
}
for (node, deps) in data.out_graph.iter(){
if deps.is_empty(){
zero_indegree.push(node.clone());
}
}
SimpleTopoSortIter {
data,
zero_indegree,
}
}
}
impl<T> Iterator for SimpleTopoSortIter<T>
where
T: Eq + std::hash::Hash + Clone,
{
type Item = Result<T, &'static str>;
fn next(&mut self) -> Option<Self::Item> {
if self.zero_indegree.is_empty() {
if self.data.out_graph.is_empty() {
return None;
}
return Some(Err("Cycle detected"));
}
let node = self.zero_indegree.pop().unwrap();
if let Some(parents) = self.data.in_graph.get(&node){
for parent in parents.iter(){
let deps = self.data.out_graph.get_mut(parent).unwrap();
deps.remove(&node);
if deps.is_empty() {
self.zero_indegree.push(parent.clone());
}
}
}
self.data.out_graph.remove(&node);
Some(Ok(node))
}
}
#[test]
fn test_sort(){
{
let mut topo_sort = SimpleTopoSort::new();
topo_sort.insert("a", []);
for node in topo_sort {
match node {
Ok(n) => assert_eq!(n, "a"),
Err(e) => panic!("err {}", e),
}
}
}
{
let mut topo_sort = SimpleTopoSort::new();
topo_sort.insert("a", ["b"]);
topo_sort.insert("b", []);
let mut iter = topo_sort.into_iter();
match iter.next() {
Some(Ok(n)) => assert_eq!(n, "b"),
_ => panic!("err"),
}
match iter.next() {
Some(Ok(n)) => assert_eq!(n, "a"),
_ => panic!("err"),
}
assert_eq!(iter.next(), None);
}
}
}

View File

@@ -6,8 +6,10 @@ pub enum Error {
SqlError(#[from] rusqlite::Error),
#[error("JSON error: {0}")]
JsonError(#[from] serde_json::Error),
#[error("Model not found {0}")]
ModelNotFound(String),
#[error("unknown error")]
Unknown,
}
pub type Result<T> = std::result::Result<T, Error>;
pub type Result<T> = std::result::Result<T, Error>;

View File

@@ -1,5 +1,6 @@
use std::fs;
use crate::error::Error::ModelNotFound;
use crate::error::Result;
use crate::models::{
CookieJar, CookieJarIden, Environment, EnvironmentIden, Folder, FolderIden, GrpcConnection,
@@ -299,7 +300,12 @@ pub async fn duplicate_grpc_request<R: Runtime>(
window: &WebviewWindow<R>,
id: &str,
) -> Result<GrpcRequest> {
let mut request = get_grpc_request(window, id).await?.clone();
let mut request = match get_grpc_request(window, id).await? {
Some(r) => r,
None => {
return Err(ModelNotFound(id.to_string()));
}
};
request.id = "".to_string();
upsert_grpc_request(window, &request).await
}
@@ -308,7 +314,12 @@ pub async fn delete_grpc_request<R: Runtime>(
window: &WebviewWindow<R>,
id: &str,
) -> Result<GrpcRequest> {
let req = get_grpc_request(window, id).await?;
let req = match get_grpc_request(window, id).await? {
Some(r) => r,
None => {
return Err(ModelNotFound(id.to_string()));
}
};
let dbm = &*window.app_handle().state::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap();
@@ -393,7 +404,10 @@ pub async fn upsert_grpc_request<R: Runtime>(
Ok(emit_upserted_model(window, m))
}
pub async fn get_grpc_request<R: Runtime>(mgr: &impl Manager<R>, id: &str) -> Result<GrpcRequest> {
pub async fn get_grpc_request<R: Runtime>(
mgr: &impl Manager<R>,
id: &str,
) -> Result<Option<GrpcRequest>> {
let dbm = &*mgr.state::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap();
@@ -403,7 +417,7 @@ pub async fn get_grpc_request<R: Runtime>(mgr: &impl Manager<R>, id: &str) -> Re
.cond_where(Expr::col(GrpcRequestIden::Id).eq(id))
.build_rusqlite(SqliteQueryBuilder);
let mut stmt = db.prepare(sql.as_str())?;
Ok(stmt.query_row(&*params.as_params(), |row| row.try_into())?)
Ok(stmt.query_row(&*params.as_params(), |row| row.try_into()).optional()?)
}
pub async fn list_grpc_requests<R: Runtime>(
@@ -509,7 +523,7 @@ pub async fn get_grpc_connection<R: Runtime>(
Ok(stmt.query_row(&*params.as_params(), |row| row.try_into())?)
}
pub async fn list_grpc_connections<R: Runtime>(
pub async fn list_grpc_connections_for_workspace<R: Runtime>(
mgr: &impl Manager<R>,
workspace_id: &str,
) -> Result<Vec<GrpcConnection>> {
@@ -573,6 +587,16 @@ pub async fn delete_all_grpc_connections<R: Runtime>(
Ok(())
}
pub async fn delete_all_grpc_connections_for_workspace<R: Runtime>(
window: &WebviewWindow<R>,
workspace_id: &str,
) -> Result<()> {
for r in list_grpc_connections_for_workspace(window, workspace_id).await? {
delete_grpc_connection(window, &r.id).await?;
}
Ok(())
}
pub async fn upsert_grpc_event<R: Runtime>(
window: &WebviewWindow<R>,
event: &GrpcEvent,
@@ -1073,7 +1097,10 @@ pub async fn duplicate_http_request<R: Runtime>(
window: &WebviewWindow<R>,
id: &str,
) -> Result<HttpRequest> {
let mut request = get_http_request(window, id).await?.clone();
let mut request = match get_http_request(window, id).await? {
None => return Err(ModelNotFound(id.to_string())),
Some(r) => r,
};
request.id = "".to_string();
upsert_http_request(window, request).await
}
@@ -1171,7 +1198,10 @@ pub async fn list_http_requests<R: Runtime>(
Ok(items.map(|v| v.unwrap()).collect())
}
pub async fn get_http_request<R: Runtime>(mgr: &impl Manager<R>, id: &str) -> Result<HttpRequest> {
pub async fn get_http_request<R: Runtime>(
mgr: &impl Manager<R>,
id: &str,
) -> Result<Option<HttpRequest>> {
let dbm = &*mgr.state::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap();
@@ -1181,14 +1211,17 @@ pub async fn get_http_request<R: Runtime>(mgr: &impl Manager<R>, id: &str) -> Re
.cond_where(Expr::col(HttpRequestIden::Id).eq(id))
.build_rusqlite(SqliteQueryBuilder);
let mut stmt = db.prepare(sql.as_str())?;
Ok(stmt.query_row(&*params.as_params(), |row| row.try_into())?)
Ok(stmt.query_row(&*params.as_params(), |row| row.try_into()).optional()?)
}
pub async fn delete_http_request<R: Runtime>(
window: &WebviewWindow<R>,
id: &str,
) -> Result<HttpRequest> {
let req = get_http_request(window, id).await?;
let req = match get_http_request(window, id).await? {
None => return Err(ModelNotFound(id.to_string())),
Some(r) => r,
};
// DB deletes will cascade but this will delete the files
delete_all_http_responses_for_request(window, id).await?;
@@ -1248,7 +1281,10 @@ pub async fn create_http_response<R: Runtime>(
delete_http_response(window, response.id.as_str()).await?;
}
let req = get_http_request(window, request_id).await?;
let req = match get_http_request(window, request_id).await? {
None => return Err(ModelNotFound(request_id.to_string())),
Some(r) => r,
};
let id = generate_model_id(ModelType::TypeHttpResponse);
let dbm = &*window.app_handle().state::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap();
@@ -1433,7 +1469,17 @@ pub async fn delete_all_http_responses_for_request<R: Runtime>(
Ok(())
}
pub async fn list_http_responses<R: Runtime>(
pub async fn delete_all_http_responses_for_workspace<R: Runtime>(
window: &WebviewWindow<R>,
workspace_id: &str,
) -> Result<()> {
for r in list_http_responses_for_workspace(window, workspace_id, None).await? {
delete_http_response(window, &r.id).await?;
}
Ok(())
}
pub async fn list_http_responses_for_workspace<R: Runtime>(
mgr: &impl Manager<R>,
workspace_id: &str,
limit: Option<i64>,

View File

@@ -273,9 +273,9 @@ impl PluginManager {
Ok(())
}
pub async fn subscribe(&self) -> (String, mpsc::Receiver<InternalEvent>) {
pub async fn subscribe(&self, label: &str) -> (String, mpsc::Receiver<InternalEvent>) {
let (tx, rx) = mpsc::channel(128);
let rx_id = generate_id();
let rx_id = format!("{label}_{}", generate_id());
self.subscribers.lock().await.insert(rx_id.clone(), tx);
(rx_id, rx)
}
@@ -362,7 +362,8 @@ impl PluginManager {
payload: &InternalEventPayload,
plugins: Vec<PluginHandle>,
) -> Result<Vec<InternalEvent>> {
let (rx_id, mut rx) = self.subscribe().await;
let label = format!("wait[{}]", plugins.len());
let (rx_id, mut rx) = self.subscribe(label.as_str()).await;
// 1. Build the events with IDs and everything
let events_to_send = plugins
@@ -557,9 +558,9 @@ impl PluginManager {
content_type: &str,
) -> Result<FilterResponse> {
let plugin_name = if content_type.to_lowercase().contains("json") {
"filter-jsonpath"
"@yaakapp/filter-jsonpath"
} else {
"filter-xpath"
"@yaakapp/filter-xpath"
};
let plugin = self

View File

@@ -0,0 +1,304 @@
enum FormatState {
TemplateTag,
String,
None,
}
/// Formats JSON that might contain template tags (skipped entirely)
pub fn format_json(text: &str, tab: &str) -> String {
let mut chars = text.chars().peekable();
let mut new_json = "".to_string();
let mut depth = 0;
let mut state = FormatState::None;
loop {
let rest_of_chars = chars.clone();
let current_char = match chars.next() {
None => break,
Some(c) => c,
};
// Handle JSON string states
if let FormatState::String = state {
match current_char {
'"' => {
state = FormatState::None;
new_json.push(current_char);
continue;
}
'\\' => {
new_json.push(current_char);
if let Some(c) = chars.next() {
new_json.push(c);
}
continue;
}
_ => {
new_json.push(current_char);
continue;
}
}
}
// Close Template tag states
if let FormatState::TemplateTag = state {
if rest_of_chars.take(2).collect::<String>() == "]}" {
state = FormatState::None;
new_json.push_str("]}");
chars.next(); // Skip the second closing bracket
continue;
} else {
new_json.push(current_char);
continue;
}
}
if rest_of_chars.take(3).collect::<String>() == "${[" {
state = FormatState::TemplateTag;
new_json.push_str("${[");
chars.next(); // Skip {
chars.next(); // Skip [
continue;
}
match current_char {
',' => {
new_json.push(current_char);
new_json.push('\n');
new_json.push_str(tab.to_string().repeat(depth).as_str());
}
'{' => match chars.peek() {
Some('}') => {
new_json.push(current_char);
new_json.push('}');
chars.next(); // Skip }
}
_ => {
depth += 1;
new_json.push(current_char);
new_json.push('\n');
new_json.push_str(tab.to_string().repeat(depth).as_str());
}
},
'[' => match chars.peek() {
Some(']') => {
new_json.push(current_char);
new_json.push(']');
chars.next(); // Skip ]
}
_ => {
depth += 1;
new_json.push(current_char);
new_json.push('\n');
new_json.push_str(tab.to_string().repeat(depth).as_str());
}
},
'}' => {
// Guard just in case invalid JSON has more closes than opens
if depth > 0 {
depth -= 1;
}
new_json.push('\n');
new_json.push_str(tab.to_string().repeat(depth).as_str());
new_json.push(current_char);
}
']' => {
// Guard just in case invalid JSON has more closes than opens
if depth > 0 {
depth -= 1;
}
new_json.push('\n');
new_json.push_str(tab.to_string().repeat(depth).as_str());
new_json.push(current_char);
}
':' => {
new_json.push(current_char);
new_json.push(' '); // Pad with space
}
'"' => {
state = FormatState::String;
new_json.push(current_char);
}
_ => {
if current_char == ' '
|| current_char == '\n'
|| current_char == '\t'
|| current_char == '\r'
{
// Don't add these
} else {
new_json.push(current_char);
}
}
}
}
// Replace only lines containing whitespace with nothing
new_json
.lines()
.filter(|line| !line.trim().is_empty()) // Filter out whitespace-only lines
.collect::<Vec<&str>>() // Collect the non-empty lines into a vector
.join("\n") // Join the lines back into a single string
}
#[cfg(test)]
mod test {
use crate::format::format_json;
#[test]
fn test_simple_object() {
assert_eq!(
format_json(r#"{"foo":"bar","baz":"qux"}"#, " "),
r#"
{
"foo": "bar",
"baz": "qux"
}
"#
.trim()
);
}
#[test]
fn test_escaped() {
assert_eq!(
format_json(r#"{"foo":"Hi \"world!\""}"#, " "),
r#"
{
"foo": "Hi \"world!\""
}
"#
.trim()
);
}
#[test]
fn test_simple_array() {
assert_eq!(
format_json(r#"["foo","bar","baz","qux"]"#, " "),
r#"
[
"foo",
"bar",
"baz",
"qux"
]
"#
.trim()
);
}
#[test]
fn test_extra_whitespace() {
assert_eq!(
format_json(
r#"["foo", "bar", "baz","qux"
]"#,
" "
),
r#"
[
"foo",
"bar",
"baz",
"qux"
]
"#
.trim()
);
}
#[test]
fn test_invalid_json() {
assert_eq!(
format_json(r#"["foo", {"bar", }"baz",["qux" ]]"#, " "),
r#"
[
"foo",
{
"bar",
}"baz",
[
"qux"
]
]
"#
.trim()
);
}
#[test]
fn test_skip_template_tags() {
assert_eq!(
format_json(r#"{"foo":${[ fn("hello", "world") ]} }"#, " "),
r#"
{
"foo": ${[ fn("hello", "world") ]}
}
"#
.trim()
);
}
#[test]
fn test_graphql_response() {
assert_eq!(
format_json(
r#"{"data":{"capsules":[{"landings":null,"original_launch":null,"reuse_count":0,"status":"retired","type":"Dragon 1.0","missions":null},{"id":"5e9e2c5bf3591882af3b2665","landings":null,"original_launch":null,"reuse_count":0,"status":"retired","type":"Dragon 1.0","missions":null}]}}"#,
" "
),
r#"
{
"data": {
"capsules": [
{
"landings": null,
"original_launch": null,
"reuse_count": 0,
"status": "retired",
"type": "Dragon 1.0",
"missions": null
},
{
"id": "5e9e2c5bf3591882af3b2665",
"landings": null,
"original_launch": null,
"reuse_count": 0,
"status": "retired",
"type": "Dragon 1.0",
"missions": null
}
]
}
}
"#
.trim()
);
}
#[test]
fn test_immediate_close() {
assert_eq!(
format_json(r#"{"bar":[]}"#, " "),
r#"
{
"bar": []
}
"#
.trim()
);
}
#[test]
fn test_more_closes() {
assert_eq!(
format_json(r#"{}}"#, " "),
r#"
{}
}
"#
.trim()
);
}
}

View File

@@ -1,5 +1,6 @@
pub mod parser;
pub mod renderer;
pub mod format;
pub use parser::*;
pub use renderer::*;

View File

@@ -1,10 +1,6 @@
import { lazy } from 'react';
import { createBrowserRouter, Navigate, RouterProvider, useParams } from 'react-router-dom';
import { routePaths, useAppRoutes } from '../hooks/useAppRoutes';
import { useGenerateThemeCss } from '../hooks/useGenerateThemeCss';
import { useSyncFontSizeSetting } from '../hooks/useSyncFontSizeSetting';
import { useSyncModelStores } from '../hooks/useSyncModelStores';
import { useSyncZoomSetting } from '../hooks/useSyncZoomSetting';
import { paths, useAppRoutes } from '../hooks/useAppRoutes';
import { DefaultLayout } from './DefaultLayout';
import { RedirectToLatestWorkspace } from './RedirectToLatestWorkspace';
import RouteError from './RouteError';
@@ -23,19 +19,23 @@ const router = createBrowserRouter([
element: <RedirectToLatestWorkspace />,
},
{
path: routePaths.workspaces(),
path: paths.workspaces(),
element: <RedirectToLatestWorkspace />,
},
{
path: routePaths.workspace({
path: paths.workspace({
workspaceId: ':workspaceId',
environmentId: null,
cookieJarId: null,
}),
element: <LazyWorkspace />,
},
{
path: routePaths.request({
path: paths.request({
workspaceId: ':workspaceId',
requestId: ':requestId',
environmentId: null,
cookieJarId: null,
}),
element: <LazyWorkspace />,
},
@@ -44,8 +44,10 @@ const router = createBrowserRouter([
element: <RedirectLegacyEnvironmentURLs />,
},
{
path: routePaths.workspaceSettings({
path: paths.workspaceSettings({
workspaceId: ':workspaceId',
environmentId: null,
cookieJarId: null,
}),
element: <LazySettings />,
},
@@ -54,12 +56,6 @@ const router = createBrowserRouter([
]);
export function AppRouter() {
// Add some global hooks that should remain persistent
useSyncModelStores();
useSyncZoomSetting();
useSyncFontSizeSetting();
useGenerateThemeCss();
return <RouterProvider router={router} />;
}
@@ -74,13 +70,13 @@ function RedirectLegacyEnvironmentURLs() {
workspaceId?: string;
environmentId?: string;
}>();
const environmentId = rawEnvironmentId === '__default__' ? undefined : rawEnvironmentId;
const environmentId = (rawEnvironmentId === '__default__' ? undefined : rawEnvironmentId) ?? null;
let to;
if (workspaceId != null && requestId != null) {
to = routes.paths.request({ workspaceId, environmentId, requestId });
to = routes.paths.request({ workspaceId, environmentId, requestId, cookieJarId: null });
} else if (workspaceId != null) {
to = routes.paths.workspace({ workspaceId, environmentId });
to = routes.paths.workspace({ workspaceId, environmentId, cookieJarId: null });
} else {
to = routes.paths.workspaces();
}

View File

@@ -1,6 +1,7 @@
import type { KeyboardEvent, ReactNode } from 'react';
import type { HotkeyAction } from '../hooks/useHotKey';
import classNames from 'classnames';
import { fuzzyFilter } from 'fuzzbunny';
import type { KeyboardEvent, ReactNode } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useActiveCookieJar } from '../hooks/useActiveCookieJar';
import { useActiveEnvironment } from '../hooks/useActiveEnvironment';
@@ -13,7 +14,6 @@ import { useCreateWorkspace } from '../hooks/useCreateWorkspace';
import { useDebouncedState } from '../hooks/useDebouncedState';
import { useDeleteRequest } from '../hooks/useDeleteRequest';
import { useEnvironments } from '../hooks/useEnvironments';
import type { HotkeyAction } from '../hooks/useHotKey';
import { useHotKey } from '../hooks/useHotKey';
import { useHttpRequestActions } from '../hooks/useHttpRequestActions';
import { useOpenSettings } from '../hooks/useOpenSettings';
@@ -271,7 +271,8 @@ export function CommandPalette({ onClose }: { onClose: () => void }) {
return routes.navigate('request', {
workspaceId: r.workspaceId,
requestId: r.id,
environmentId: activeEnvironment?.id,
environmentId: activeEnvironment?.id ?? null,
cookieJarId: activeCookieJar?.id ?? null,
});
},
});
@@ -313,7 +314,8 @@ export function CommandPalette({ onClose }: { onClose: () => void }) {
workspaceCommands,
sortedRequests,
routes,
activeEnvironment,
activeEnvironment?.id,
activeCookieJar?.id,
sortedEnvironments,
setActiveEnvironmentId,
sortedWorkspaces,
@@ -335,7 +337,9 @@ export function CommandPalette({ onClose }: { onClose: () => void }) {
})),
command,
{ fields: ['filterBy'] },
).map((v) => v.item)
)
.sort((a, b) => b.score - a.score)
.map((v) => v.item)
: allItems;
const filteredGroups = groups

View File

@@ -45,6 +45,7 @@ export const EnvironmentEditDialog = function ({ initialEnvironment }: Props) {
const handleCreateEnvironment = async () => {
const e = await createEnvironment.mutateAsync();
if (e == null) return;
setSelectedEnvironmentId(e.id);
};

View File

@@ -2,6 +2,7 @@ import { emit } from '@tauri-apps/api/event';
import type { PromptTextRequest, PromptTextResponse } from '@yaakapp-internal/plugin';
import { useEnsureActiveCookieJar } from '../hooks/useActiveCookieJar';
import { useActiveWorkspaceChangedToast } from '../hooks/useActiveWorkspaceChangedToast';
import {useGenerateThemeCss} from "../hooks/useGenerateThemeCss";
import { useHotKey } from '../hooks/useHotKey';
import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
import { useNotificationToast } from '../hooks/useNotificationToast';
@@ -10,10 +11,18 @@ import { useRecentCookieJars } from '../hooks/useRecentCookieJars';
import { useRecentEnvironments } from '../hooks/useRecentEnvironments';
import { useRecentRequests } from '../hooks/useRecentRequests';
import { useRecentWorkspaces } from '../hooks/useRecentWorkspaces';
import {useSyncFontSizeSetting} from "../hooks/useSyncFontSizeSetting";
import {useSyncModelStores} from "../hooks/useSyncModelStores";
import { useSyncWorkspaceChildModels } from '../hooks/useSyncWorkspaceChildModels';
import {useSyncZoomSetting} from "../hooks/useSyncZoomSetting";
import { useToggleCommandPalette } from '../hooks/useToggleCommandPalette';
export function GlobalHooks() {
useSyncModelStores();
useSyncZoomSetting();
useSyncFontSizeSetting();
useGenerateThemeCss();
// Include here so they always update, even if no component references them
useRecentWorkspaces();
useRecentEnvironments();

View File

@@ -2,12 +2,15 @@ import type { HttpRequest } from '@yaakapp-internal/models';
import { updateSchema } from 'cm6-graphql';
import type { EditorView } from 'codemirror';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useLocalStorage } from 'react-use';
import { useIntrospectGraphQL } from '../hooks/useIntrospectGraphQL';
import { tryFormatJson } from '../lib/formatters';
import { Button } from './core/Button';
import { Dropdown } from './core/Dropdown';
import type { EditorProps } from './core/Editor';
import { Editor, formatGraphQL } from './core/Editor';
import { FormattedError } from './core/FormattedError';
import { Icon } from './core/Icon';
import { Separator } from './core/Separator';
import { useDialog } from './DialogContext';
@@ -19,27 +22,34 @@ type Props = Pick<EditorProps, 'heightMode' | 'className' | 'forceUpdateKey'> &
export function GraphQLEditor({ body, onChange, baseRequest, ...extraEditorProps }: Props) {
const editorViewRef = useRef<EditorView>(null);
const { schema, isLoading, error, refetch } = useIntrospectGraphQL(baseRequest);
const [currentBody, setCurrentBody] = useState<{ query: string; variables: string }>(() => {
// Migrate text bodies to GraphQL format
// NOTE: This is how GraphQL used to be stored
if ('text' in body) {
const b = tryParseJson(body.text, {});
const variables = JSON.stringify(b.variables ?? '', null, 2);
return { query: b.query ?? '', variables };
}
return { query: body.query ?? '', variables: body.variables ?? '' };
const [autoIntrospectDisabled, setAutoIntrospectDisabled] = useLocalStorage<
Record<string, boolean>
>('graphQLAutoIntrospectDisabled', {});
const { schema, isLoading, error, refetch, clear } = useIntrospectGraphQL(baseRequest, {
disabled: autoIntrospectDisabled?.[baseRequest.id],
});
const [currentBody, setCurrentBody] = useState<{ query: string; variables: string | undefined }>(
() => {
// Migrate text bodies to GraphQL format
// NOTE: This is how GraphQL used to be stored
if ('text' in body) {
const b = tryParseJson(body.text, {});
const variables = JSON.stringify(b.variables || undefined, null, 2);
return { query: b.query ?? '', variables };
}
return { query: body.query ?? '', variables: body.variables ?? '' };
},
);
const handleChangeQuery = (query: string) => {
const newBody = { query, variables: currentBody.variables };
const newBody = { query, variables: currentBody.variables || undefined };
setCurrentBody(newBody);
onChange(newBody);
};
const handleChangeVariables = (variables: string) => {
const newBody = { query: currentBody.query, variables };
const newBody = { query: currentBody.query, variables: variables || undefined };
setCurrentBody(newBody);
onChange(newBody);
};
@@ -52,52 +62,106 @@ export function GraphQLEditor({ body, onChange, baseRequest, ...extraEditorProps
const dialog = useDialog();
const actions = useMemo<EditorProps['actions']>(() => {
const isValid = error || isLoading;
if (!isValid) {
return [];
}
const actions: EditorProps['actions'] = [
const actions = useMemo<EditorProps['actions']>(
() => [
<div key="introspection" className="!opacity-100">
<Button
key="introspection"
size="xs"
color={error ? 'danger' : 'secondary'}
isLoading={isLoading}
onClick={() => {
dialog.show({
title: 'Introspection Failed',
size: 'dynamic',
id: 'introspection-failed',
render: () => (
<>
<FormattedError>{error ?? 'unknown'}</FormattedError>
<div className="w-full my-4">
<Button
onClick={() => {
dialog.hide('introspection-failed');
refetch();
}}
className="ml-auto"
color="primary"
size="sm"
>
Try Again
</Button>
</div>
</>
),
});
}}
>
{error ? 'Introspection Failed' : 'Introspecting'}
</Button>
{schema === undefined ? null /* Initializing */ : !error ? (
<Dropdown
items={[
{
key: 'refresh',
label: 'Refetch',
leftSlot: <Icon icon="refresh" />,
onSelect: refetch,
},
{
key: 'clear',
label: 'Clear',
onSelect: clear,
hidden: !schema,
variant: 'danger',
leftSlot: <Icon icon="trash" />,
},
{ type: 'separator', label: 'Setting' },
{
key: 'auto_fetch',
label: 'Automatic Introspection',
onSelect: () => {
setAutoIntrospectDisabled({
...autoIntrospectDisabled,
[baseRequest.id]: !autoIntrospectDisabled?.[baseRequest.id],
});
},
leftSlot: (
<Icon
icon={
autoIntrospectDisabled?.[baseRequest.id]
? 'check_square_unchecked'
: 'check_square_checked'
}
/>
),
},
]}
>
<Button
size="sm"
variant="border"
title="Refetch Schema"
isLoading={isLoading}
color={isLoading || schema ? 'default' : 'warning'}
>
{isLoading ? 'Introspecting' : schema ? 'Schema' : 'No Schema'}
</Button>
</Dropdown>
) : (
<Button
size="sm"
color="danger"
isLoading={isLoading}
onClick={() => {
dialog.show({
title: 'Introspection Failed',
size: 'dynamic',
id: 'introspection-failed',
render: ({ hide }) => (
<>
<FormattedError>{error ?? 'unknown'}</FormattedError>
<div className="w-full my-4">
<Button
onClick={async () => {
hide();
await refetch();
}}
className="ml-auto"
color="primary"
size="sm"
>
Try Again
</Button>
</div>
</>
),
});
}}
>
Introspection Failed
</Button>
)}
</div>,
];
return actions;
}, [dialog, error, isLoading, refetch]);
],
[
isLoading,
refetch,
error,
autoIntrospectDisabled,
baseRequest.id,
clear,
schema,
setAutoIntrospectDisabled,
dialog,
],
);
return (
<div className="h-full w-full grid grid-cols-1 grid-rows-[minmax(0,100%)_auto]">

View File

@@ -71,7 +71,11 @@ export function MoveToWorkspaceDialog({ onDone, request, activeWorkspaceId }: Pr
className="mr-auto min-w-[5rem]"
onClick={() => {
toast.hide('workspace-moved');
routes.navigate('workspace', { workspaceId: selectedWorkspaceId });
routes.navigate('workspace', {
workspaceId: selectedWorkspaceId,
cookieJarId: null,
environmentId: null,
});
}}
>
Switch to Workspace

View File

@@ -1,6 +1,7 @@
import classNames from 'classnames';
import { useMemo, useRef } from 'react';
import { useKeyPressEvent } from 'react-use';
import { useActiveCookieJar } from '../hooks/useActiveCookieJar';
import { useActiveEnvironment } from '../hooks/useActiveEnvironment';
import { useActiveRequest } from '../hooks/useActiveRequest';
import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
@@ -20,6 +21,7 @@ export function RecentRequestsDropdown({ className }: Pick<ButtonProps, 'classNa
const activeRequest = useActiveRequest();
const activeWorkspace = useActiveWorkspace();
const [activeEnvironment] = useActiveEnvironment();
const [activeCookieJar] = useActiveCookieJar();
const routes = useAppRoutes();
const allRecentRequestIds = useRecentRequests();
const recentRequestIds = useMemo(() => allRecentRequestIds.slice(1), [allRecentRequestIds]);
@@ -57,8 +59,9 @@ export function RecentRequestsDropdown({ className }: Pick<ButtonProps, 'classNa
onSelect: () => {
routes.navigate('request', {
requestId: request.id,
environmentId: activeEnvironment?.id,
workspaceId: activeWorkspace.id,
environmentId: activeEnvironment?.id ?? null,
cookieJarId: activeCookieJar?.id ?? null,
});
},
});
@@ -76,7 +79,7 @@ export function RecentRequestsDropdown({ className }: Pick<ButtonProps, 'classNa
}
return recentRequestItems.slice(0, 20);
}, [activeWorkspace, activeEnvironment?.id, recentRequestIds, requests, routes]);
}, [activeWorkspace, recentRequestIds, requests, routes, activeEnvironment?.id, activeCookieJar?.id]);
return (
<Dropdown ref={dropdownRef} items={items}>

View File

@@ -41,7 +41,7 @@ export const RecentResponsesDropdown = function ResponsePane({
},
{
key: 'copy',
label: 'Copy to Clipboard',
label: 'Copy Body',
onSelect: copyResponse.mutate,
leftSlot: <Icon icon="copy" />,
hidden: responses.length === 0,

View File

@@ -1,6 +1,7 @@
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAppRoutes } from '../hooks/useAppRoutes';
import { getRecentCookieJars } from '../hooks/useRecentCookieJars';
import { getRecentEnvironments } from '../hooks/useRecentEnvironments';
import { getRecentRequests } from '../hooks/useRecentRequests';
import { useRecentWorkspaces } from '../hooks/useRecentWorkspaces';
@@ -20,13 +21,14 @@ export function RedirectToLatestWorkspace() {
(async function () {
const workspaceId = recentWorkspaces[0] ?? workspaces[0]?.id ?? 'n/a';
const environmentId = (await getRecentEnvironments(workspaceId))[0];
const requestId = (await getRecentRequests(workspaceId))[0];
const environmentId = (await getRecentEnvironments(workspaceId))[0] ?? null;
const cookieJarId = (await getRecentCookieJars(workspaceId))[0] ?? null;
const requestId = (await getRecentRequests(workspaceId))[0] ?? null;
if (workspaceId != null && requestId != null) {
navigate(routes.paths.request({ workspaceId, environmentId, requestId }));
navigate(routes.paths.request({ workspaceId, environmentId, requestId, cookieJarId }));
} else {
navigate(routes.paths.workspace({ workspaceId, environmentId }));
navigate(routes.paths.workspace({ workspaceId, environmentId, cookieJarId }));
}
})();
}, [navigate, recentWorkspaces, routes.paths, workspaces, workspaces.length]);

View File

@@ -13,6 +13,7 @@ import React, { Fragment, useCallback, useMemo, useRef, useState } from 'react';
import type { XYCoord } from 'react-dnd';
import { useDrag, useDrop } from 'react-dnd';
import { useKey, useKeyPressEvent } from 'react-use';
import { useActiveCookieJar } from '../hooks/useActiveCookieJar';
import { useActiveEnvironment } from '../hooks/useActiveEnvironment';
import { useActiveRequest } from '../hooks/useActiveRequest';
@@ -73,6 +74,7 @@ export function Sidebar({ className }: Props) {
const sidebarRef = useRef<HTMLLIElement>(null);
const activeRequest = useActiveRequest();
const [activeEnvironment] = useActiveEnvironment();
const [activeCookieJar] = useActiveCookieJar();
const folders = useFolders();
const requests = useRequests();
const activeWorkspace = useActiveWorkspace();
@@ -221,14 +223,22 @@ export function Sidebar({ className }: Props) {
routes.navigate('request', {
requestId: id,
workspaceId: item.workspaceId,
environmentId: activeEnvironment?.id,
environmentId: activeEnvironment?.id ?? null,
cookieJarId: activeCookieJar?.id ?? null,
});
setSelectedId(id);
setSelectedTree(tree);
if (!opts.noFocus) focusActiveRequest({ forced: { id, tree } });
}
},
[treeParentMap, collapsed, routes, activeEnvironment, focusActiveRequest],
[
treeParentMap,
collapsed,
routes,
activeEnvironment?.id,
activeCookieJar?.id,
focusActiveRequest,
],
);
const handleClearSelected = useCallback(() => {
@@ -273,8 +283,9 @@ export function Sidebar({ className }: Props) {
e.preventDefault();
routes.navigate('request', {
requestId: selected.id,
workspaceId: activeWorkspace?.id,
environmentId: activeEnvironment?.id,
workspaceId: activeWorkspace?.id ?? null,
environmentId: activeEnvironment?.id ?? null,
cookieJarId: activeCookieJar?.id ?? null,
});
});
@@ -702,11 +713,17 @@ function SidebarItem({
useScrollIntoView(ref.current, isActive);
const handleSubmitNameEdit = useCallback(
(el: HTMLInputElement) => {
async (el: HTMLInputElement) => {
if (itemModel === 'http_request') {
updateHttpRequest.mutate({ id: itemId, update: (r) => ({ ...r, name: el.value }) });
await updateHttpRequest.mutateAsync({
id: itemId,
update: (r) => ({ ...r, name: el.value }),
});
} else if (itemModel === 'grpc_request') {
updateGrpcRequest.mutate({ id: itemId, update: (r) => ({ ...r, name: el.value }) });
await updateGrpcRequest.mutateAsync({
id: itemId,
update: (r) => ({ ...r, name: el.value }),
});
}
setEditing(false);
},
@@ -724,7 +741,7 @@ function SidebarItem({
switch (e.key) {
case 'Enter':
e.preventDefault();
handleSubmitNameEdit(e.currentTarget);
await handleSubmitNameEdit(e.currentTarget);
break;
case 'Escape':
e.preventDefault();
@@ -741,8 +758,8 @@ function SidebarItem({
}, [setEditing, itemModel]);
const handleBlur = useCallback(
(e: React.FocusEvent<HTMLInputElement>) => {
handleSubmitNameEdit(e.currentTarget);
async (e: React.FocusEvent<HTMLInputElement>) => {
await handleSubmitNameEdit(e.currentTarget);
},
[handleSubmitNameEdit],
);
@@ -903,8 +920,9 @@ function SidebarItem({
className={classNames(
'w-full flex gap-1.5 items-center h-xs px-1.5 rounded-md focus-visible:ring focus-visible:ring-border-focus outline-0',
editing && 'ring-1 focus-within:ring-focus',
isActive && 'bg-surface-highlight text',
!isActive && 'text-text-subtle group-hover/item:text-text active:bg-surface-highlight',
isActive && 'bg-surface-highlight text-text',
!isActive && 'text-text-subtle group-hover/item:text-text',
showContextMenu && '!text-text', // Show as "active" when context menu is open
selected && useProminentStyles && '!bg-surface-active',
)}
>

View File

@@ -1,3 +1,4 @@
import type { Folder, HttpRequest } from '@yaakapp-internal/models';
import type {
TemplateFunction,
TemplateFunctionArg,
@@ -12,6 +13,7 @@ import classNames from 'classnames';
import { useCallback, useMemo, useState } from 'react';
import { useActiveRequest } from '../hooks/useActiveRequest';
import { useDebouncedValue } from '../hooks/useDebouncedValue';
import { useFolders } from '../hooks/useFolders';
import { useHttpRequests } from '../hooks/useHttpRequests';
import { useRenderTemplate } from '../hooks/useRenderTemplate';
import { useTemplateTokensToString } from '../hooks/useTemplateTokensToString';
@@ -253,6 +255,7 @@ function HttpRequestArg({
value: string;
onChange: (v: string) => void;
}) {
const folders = useFolders();
const httpRequests = useHttpRequests();
const activeRequest = useActiveRequest();
return (
@@ -262,15 +265,35 @@ function HttpRequestArg({
onChange={onChange}
value={value}
options={[
...httpRequests.map((r) => ({
label: fallbackRequestName(r) + (activeRequest?.id === r.id ? ' (current)' : ''),
value: r.id,
})),
...httpRequests.map((r) => {
return {
label: buildRequestBreadcrumbs(r, folders).join(' / ') + (r.id == activeRequest?.id ? ' (current)' : ''),
value: r.id,
};
}),
]}
/>
);
}
function buildRequestBreadcrumbs(request: HttpRequest, folders: Folder[]): string[] {
const ancestors: (HttpRequest | Folder)[] = [request];
const next = () => {
const latest = ancestors[0];
if (latest == null) return [];
const parent = folders.find((f) => f.id === latest.folderId);
if (parent == null) return;
ancestors.unshift(parent);
next();
};
next();
return ancestors.map((a) => (a.model === 'folder' ? a.name : fallbackRequestName(a)));
}
function CheckboxArg({
arg,
onChange,

View File

@@ -2,6 +2,7 @@ import classNames from 'classnames';
import { memo, useCallback, useMemo } from 'react';
import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
import { useCreateWorkspace } from '../hooks/useCreateWorkspace';
import { useDeleteSendHistory } from '../hooks/useDeleteSendHistory';
import { useDeleteWorkspace } from '../hooks/useDeleteWorkspace';
import { useOpenWorkspace } from '../hooks/useOpenWorkspace';
import { usePrompt } from '../hooks/usePrompt';
@@ -36,6 +37,7 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
const settings = useSettings();
const openWorkspace = useOpenWorkspace();
const openWorkspaceNewWindow = settings?.openWorkspaceNewWindow ?? null;
const deleteSendHistory = useDeleteSendHistory();
const { workspaceItems, extraItems } = useMemo<{
workspaceItems: RadioDropdownItem[];
@@ -70,9 +72,15 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
updateWorkspace.mutate({ name });
},
},
{
key: 'delete-responses',
label: 'Clear Send History',
leftSlot: <Icon icon="history" />,
onSelect: deleteSendHistory.mutate,
},
{
key: 'delete',
label: 'Delete',
label: 'Delete Workspace',
leftSlot: <Icon icon="trash" />,
onSelect: deleteWorkspace.mutate,
variant: 'danger',
@@ -90,7 +98,8 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
}, [
activeWorkspace?.name,
activeWorkspaceId,
createWorkspace,
createWorkspace.mutate,
deleteSendHistory.mutate,
deleteWorkspace.mutate,
prompt,
updateWorkspace,

View File

@@ -350,8 +350,9 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle'>, MenuPro
container: CSSProperties;
menu: CSSProperties;
triangle: CSSProperties;
upsideDown: boolean;
}>(() => {
if (triggerShape == null) return { container: {}, triangle: {}, menu: {} };
if (triggerShape == null) return { container: {}, triangle: {}, menu: {}, upsideDown: false };
const menuMarginY = 5;
const docRect = document.documentElement.getBoundingClientRect();
@@ -364,6 +365,7 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle'>, MenuPro
const upsideDown = heightBelow < heightAbove && heightBelow < items.length * 25 + 20 + 200;
const triggerWidth = triggerShape.right - triggerShape.left;
return {
upsideDown,
container: {
top: !upsideDown ? top + menuMarginY : undefined,
bottom: upsideDown
@@ -426,7 +428,7 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle'>, MenuPro
<motion.div
tabIndex={0}
onKeyDown={handleMenuKeyDown}
initial={{ opacity: 0, y: -5, scale: 0.98 }}
initial={{ opacity: 0, y: (styles.upsideDown ? 1 : -1) * 5, scale: 0.98 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
role="menu"
aria-orientation="vertical"

View File

@@ -8,12 +8,12 @@ import classNames from 'classnames';
import { EditorView } from 'codemirror';
import type { MutableRefObject, ReactNode } from 'react';
import {
useEffect,
Children,
cloneElement,
forwardRef,
isValidElement,
useCallback,
useEffect,
useImperativeHandle,
useMemo,
useRef,
@@ -61,7 +61,7 @@ export interface EditorProps {
onKeyDown?: (e: KeyboardEvent) => void;
singleLine?: boolean;
wrapLines?: boolean;
format?: (v: string) => string;
format?: (v: string) => Promise<string>;
autocomplete?: GenericCompletionConfig;
autocompleteVariables?: boolean;
extraExtensions?: Extension[];
@@ -343,6 +343,33 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
[forceUpdateKey],
);
// For read-only mode, update content when `defaultValue` changes
useEffect(() => {
if (!readOnly || cm.current?.view == null || defaultValue == null) return;
// Replace codemirror contents
const currentDoc = cm.current.view.state.doc.toString();
if (defaultValue.startsWith(currentDoc)) {
// If we're just appending, append only the changes. This preserves
// things like scroll position.
cm.current.view.dispatch({
changes: cm.current.view.state.changes({
from: currentDoc.length,
insert: defaultValue.slice(currentDoc.length),
}),
});
} else {
// If we're replacing everything, reset the entire content
cm.current.view.dispatch({
changes: cm.current.view.state.changes({
from: 0,
to: currentDoc.length,
insert: defaultValue,
}),
});
}
}, [defaultValue, readOnly]);
// Add bg classes to actions, so they appear over the text
const decoratedActions = useMemo(() => {
const results = [];
@@ -360,10 +387,10 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
icon="magic_wand"
variant="border"
className={classNames(actionClassName)}
onClick={() => {
onClick={async () => {
if (cm.current === null) return;
const { doc } = cm.current.view.state;
const formatted = format(doc.toString());
const formatted = await format(doc.toString());
// Update editor and blur because the cursor will reset anyway
cm.current.view.dispatch({
changes: { from: 0, to: doc.length, insert: formatted },

View File

@@ -52,7 +52,7 @@ export const syntaxHighlightStyle = HighlightStyle.define([
textDecoration: 'underline',
},
{
tag: [t.paren, t.bracket, t.brace],
tag: [t.paren, t.bracket, t.squareBracket, t.brace, t.separator],
color: 'var(--textSubtle)',
},
{

View File

@@ -22,11 +22,13 @@ const icons = {
cake: lucide.CakeIcon,
chat: lucide.MessageSquare,
check: lucide.CheckIcon,
check_square_checked: lucide.SquareCheckIcon,
check_square_unchecked: lucide.SquareIcon,
check_circle: lucide.CheckCircleIcon,
chevron_down: lucide.ChevronDownIcon,
chevron_right: lucide.ChevronRightIcon,
circle_alert: lucide.CircleAlertIcon,
cloud: lucide.CloudIcon,
clock: lucide.ClockIcon,
code: lucide.CodeIcon,
cookie: lucide.CookieIcon,
copy: lucide.CopyIcon,
@@ -48,6 +50,7 @@ const icons = {
grip_vertical: lucide.GripVerticalIcon,
hand: lucide.HandIcon,
help: lucide.CircleHelpIcon,
history: lucide.HistoryIcon,
house: lucide.HomeIcon,
info: lucide.InfoIcon,
keyboard: lucide.KeyboardIcon,
@@ -55,6 +58,7 @@ const icons = {
left_panel_visible: lucide.PanelLeftCloseIcon,
magic_wand: lucide.Wand2Icon,
minus: lucide.MinusIcon,
minus_circle: lucide.MinusCircleIcon,
moon: lucide.MoonIcon,
more_vertical: lucide.MoreVerticalIcon,
paste: lucide.ClipboardPasteIcon,

View File

@@ -128,6 +128,9 @@ export const PlainInput = forwardRef<HTMLInputElement, PlainInputProps>(function
type={type === 'password' && !obscured ? 'text' : type}
defaultValue={defaultValue}
placeholder={placeholder}
autoComplete="off"
autoCapitalize="off"
autoCorrect="off"
onChange={(e) => handleChange(e.target.value)}
onPaste={(e) => onPaste?.(e.clipboardData.getData('Text'))}
className={inputClassName}

View File

@@ -4,10 +4,11 @@ import type { ServerSentEvent } from '@yaakapp-internal/sse';
import classNames from 'classnames';
import { motion } from 'framer-motion';
import React, { Fragment, useMemo, useRef, useState } from 'react';
import { useFormatText } from '../../hooks/useFormatText';
import { useResponseBodyEventSource } from '../../hooks/useResponseBodyEventSource';
import { isJSON } from '../../lib/contentType';
import { tryFormatJson } from '../../lib/formatters';
import { Button } from '../core/Button';
import type { EditorProps } from '../core/Editor';
import { Editor } from '../core/Editor';
import { Icon } from '../core/Icon';
import { InlineCode } from '../core/InlineCode';
@@ -65,7 +66,14 @@ function ActualEventStreamViewer({ response }: Props) {
<Separator />
</div>
<div className="pl-2 overflow-y-auto">
<div className="mb-2 select-text cursor-text font-semibold">Message Received</div>
<HStack space={1.5} className="mb-2 select-text cursor-text font-semibold">
<EventLabels
className="text-sm"
event={activeEvent}
index={activeEventIndex ?? 0}
/>
Message Received
</HStack>
{!showLarge && activeEvent.data.length > 1000 * 1000 ? (
<VStack space={2} className="italic text-text-subtlest">
Message previews larger than 1MB are hidden
@@ -88,12 +96,7 @@ function ActualEventStreamViewer({ response }: Props) {
</div>
</VStack>
) : (
<Editor
readOnly
forceUpdateKey={activeEvent.id ?? activeEvent.data}
defaultValue={tryFormatJson(activeEvent.data)}
language={language}
/>
<FormattedEditor language={language} text={activeEvent.data} />
)}
</div>
</div>
@@ -104,6 +107,12 @@ function ActualEventStreamViewer({ response }: Props) {
);
}
function FormattedEditor({ text, language }: { text: string; language: EditorProps['language'] }) {
const formatted = useFormatText({ text, language, pretty: true });
if (formatted.data == null) return null;
return <Editor readOnly defaultValue={formatted.data} language={language} />;
}
function EventStreamEventsVirtual({
events,
activeEventIndex,
@@ -149,6 +158,7 @@ function EventStreamEventsVirtual({
<EventStreamEvent
event={event}
isActive={virtualItem.index === activeEventIndex}
index={virtualItem.index}
onClick={() => {
if (virtualItem.index === activeEventIndex) setActiveEventIndex(null);
else setActiveEventIndex(virtualItem.index);
@@ -167,11 +177,13 @@ function EventStreamEvent({
isActive,
event,
className,
index,
}: {
onClick: () => void;
isActive: boolean;
event: ServerSentEvent;
className?: string;
index: number;
}) {
return (
<motion.button
@@ -187,19 +199,33 @@ function EventStreamEvent({
)}
>
<Icon className={classNames('text-info')} title="Server Message" icon="arrow_big_down_dash" />
<HStack space={1.5} className="text-sm">
{event.eventType && (
<InlineCode className={classNames('py-0', isActive && 'bg-text-subtlest text-text')}>
{event.eventType}
</InlineCode>
)}
{event.id && (
<InlineCode className={classNames('py-0', isActive && 'bg-text-subtlest text-text')}>
{event.id}
</InlineCode>
)}
</HStack>
<EventLabels className="text-sm" event={event} isActive={isActive} index={index} />
<div className={classNames('w-full truncate text-xs')}>{event.data.slice(0, 1000)}</div>
</motion.button>
);
}
function EventLabels({
className,
event,
index,
isActive,
}: {
event: ServerSentEvent;
index: number;
className: string;
isActive?: boolean;
}) {
return (
<HStack space={1.5} alignItems="center" className={className}>
<InlineCode className={classNames('py-0', isActive && 'bg-text-subtlest text-text')}>
{event.id ?? index}
</InlineCode>
{event.eventType && (
<InlineCode className={classNames('py-0', isActive && 'bg-text-subtlest text-text')}>
{event.eventType}
</InlineCode>
)}
</HStack>
);
}

View File

@@ -31,7 +31,7 @@ export function HTMLOrTextViewer({ response, pretty, textViewerClassName }: Prop
}
if (language === 'html' && pretty) {
return <WebPageViewer response={response} />;
return <WebPageViewer response={response}/>;
} else {
return (
<TextViewer
@@ -41,6 +41,7 @@ export function HTMLOrTextViewer({ response, pretty, textViewerClassName }: Prop
className={textViewerClassName}
onSaveResponse={saveResponse.mutate}
responseId={response.id}
requestId={response.requestId}
/>
);
}

View File

@@ -5,8 +5,8 @@ import { createGlobalState } from 'react-use';
import { useCopy } from '../../hooks/useCopy';
import { useDebouncedValue } from '../../hooks/useDebouncedValue';
import { useFilterResponse } from '../../hooks/useFilterResponse';
import { useFormatText } from '../../hooks/useFormatText';
import { useToggle } from '../../hooks/useToggle';
import { tryFormatJson, tryFormatXml } from '../../lib/formatters';
import { CopyButton } from '../CopyButton';
import { Banner } from '../core/Banner';
import { Button } from '../core/Button';
@@ -28,6 +28,7 @@ interface Props {
text: string;
language: EditorProps['language'];
responseId: string;
requestId: string;
onSaveResponse: () => void;
}
@@ -37,20 +38,21 @@ export function TextViewer({
language,
text,
responseId,
requestId,
pretty,
className,
onSaveResponse,
}: Props) {
const [filterTextMap, setFilterTextMap] = useFilterText();
const [showLargeResponse, toggleShowLargeResponse] = useToggle();
const filterText = filterTextMap[responseId] ?? null;
const filterText = filterTextMap[requestId] ?? null;
const copy = useCopy();
const debouncedFilterText = useDebouncedValue(filterText, 200);
const setFilterText = useCallback(
(v: string | null) => {
setFilterTextMap((m) => ({ ...m, [responseId]: v }));
setFilterTextMap((m) => ({ ...m, [requestId]: v }));
},
[setFilterTextMap, responseId],
[setFilterTextMap, requestId],
);
const isSearching = filterText != null;
@@ -75,7 +77,7 @@ export function TextViewer({
nodes.push(
<div key="input" className="w-full !opacity-100">
<Input
key={responseId}
key={requestId}
validate={!filteredResponse.error}
hideLabel
autoFocus
@@ -110,11 +112,13 @@ export function TextViewer({
filteredResponse.error,
isSearching,
language,
responseId,
requestId,
setFilterText,
toggleSearch,
]);
const formattedBody = useFormatText({ text, language, pretty });
if (!showLargeResponse && text.length > LARGE_RESPONSE_BYTES) {
return (
<Banner color="primary" className="h-full flex flex-col gap-3">
@@ -138,12 +142,9 @@ export function TextViewer({
);
}
const formattedBody =
pretty && language === 'json'
? tryFormatJson(text)
: pretty && (language === 'xml' || language === 'html')
? tryFormatXml(text)
: text;
if (formattedBody.data == null) {
return null;
}
let body;
if (isSearching && filterText?.length > 0) {
@@ -153,14 +154,13 @@ export function TextViewer({
body = filteredResponse.data != null ? filteredResponse.data : '';
}
} else {
body = formattedBody;
body = formattedBody.data;
}
return (
<Editor
readOnly
className={className}
forceUpdateKey={body}
defaultValue={body}
language={language}
actions={actions}

View File

@@ -18,6 +18,8 @@ export function useActiveCookieJar() {
export function useEnsureActiveCookieJar() {
const cookieJars = useCookieJars();
const [activeCookieJarId, setActiveCookieJarId] = useActiveCookieJarId();
// Set the active cookie jar to the first one, if none set
useEffect(() => {
if (cookieJars == null) return; // Hasn't loaded yet
if (cookieJars.find((j) => j.id === activeCookieJarId)) {

View File

@@ -1,7 +1,6 @@
import { useParams } from 'react-router-dom';
import type { RouteParamsRequest } from './useAppRoutes';
export function useActiveRequestId(): string | null {
const { requestId } = useParams<RouteParamsRequest>();
const { requestId } = useParams();
return requestId ?? null;
}

View File

@@ -1,7 +1,6 @@
import type { Workspace } from '@yaakapp-internal/models';
import { useMemo } from 'react';
import { useParams } from 'react-router-dom';
import type { RouteParamsWorkspace } from './useAppRoutes';
import { useWorkspaces } from './useWorkspaces';
export function useActiveWorkspace(): Workspace | null {
@@ -15,6 +14,6 @@ export function useActiveWorkspace(): Workspace | null {
}
function useActiveWorkspaceId(): string | null {
const { workspaceId } = useParams<RouteParamsWorkspace>();
const { workspaceId } = useParams();
return workspaceId ?? null;
}

View File

@@ -4,20 +4,17 @@ import { useDialog } from '../components/DialogContext';
import type { AlertProps } from './Alert';
import { Alert } from './Alert';
interface AlertArg {
id: string;
title: DialogProps['title'];
body: AlertProps['body'];
size?: DialogProps['size'];
}
export function useAlert() {
const dialog = useDialog();
return useCallback(
({
id,
title,
body,
size = 'sm',
}: {
id: string;
title: DialogProps['title'];
body: AlertProps['body'];
size?: DialogProps['size'];
}) =>
return useCallback<(a: AlertArg) => void>(
({ id, title, body, size = 'sm' }: AlertArg) =>
dialog.show({
id,
title,

View File

@@ -1,22 +1,19 @@
import type { Environment } from '@yaakapp-internal/models';
import { useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { QUERY_COOKIE_JAR_ID } from './useActiveCookieJar';
import { QUERY_ENVIRONMENT_ID } from './useActiveEnvironment';
import { useActiveRequestId } from './useActiveRequestId';
import { useActiveWorkspace } from './useActiveWorkspace';
export type RouteParamsWorkspace = {
workspaceId: string;
environmentId?: string;
cookieJarId?: string;
environmentId: string | null;
cookieJarId: string | null;
};
export type RouteParamsRequest = RouteParamsWorkspace & {
requestId: string;
};
export const routePaths = {
export const paths = {
workspaces() {
return '/workspaces';
},
@@ -52,44 +49,21 @@ export const routePaths = {
};
export function useAppRoutes() {
const activeWorkspace = useActiveWorkspace();
const requestId = useActiveRequestId();
const nav = useNavigate();
const navigate = useCallback(
<T extends keyof typeof routePaths>(path: T, ...params: Parameters<(typeof routePaths)[T]>) => {
<T extends keyof typeof paths>(path: T, ...params: Parameters<(typeof paths)[T]>) => {
// Not sure how to make TS work here, but it's good from the
// outside caller perspective.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const resolvedPath = routePaths[path](...(params as any));
const resolvedPath = paths[path](...(params as any));
nav(resolvedPath);
},
[nav],
);
const setEnvironment = useCallback(
(environment: Environment | null) => {
if (activeWorkspace == null) {
navigate('workspaces');
} else if (requestId == null) {
navigate('workspace', {
workspaceId: activeWorkspace.id,
environmentId: environment == null ? undefined : environment.id,
});
} else {
navigate('request', {
workspaceId: activeWorkspace.id,
environmentId: environment == null ? undefined : environment.id,
requestId,
});
}
},
[navigate, activeWorkspace, requestId],
);
return {
paths: routePaths,
paths,
navigate,
setEnvironment,
};
}

View File

@@ -1,15 +1,19 @@
import { useMutation } from '@tanstack/react-query';
import type { CookieJar } from '@yaakapp-internal/models';
import {useSetAtom} from "jotai";
import { trackEvent } from '../lib/analytics';
import { invokeCmd } from '../lib/tauri';
import { useActiveWorkspace } from './useActiveWorkspace';
import {cookieJarsAtom} from "./useCookieJars";
import { usePrompt } from './usePrompt';
import {updateModelList} from "./useSyncModelStores";
export function useCreateCookieJar() {
const workspace = useActiveWorkspace();
const prompt = usePrompt();
const setCookieJars = useSetAtom(cookieJarsAtom);
return useMutation<CookieJar>({
return useMutation<CookieJar | null>({
mutationKey: ['create_cookie_jar'],
mutationFn: async () => {
if (workspace === null) {
@@ -23,8 +27,16 @@ export function useCreateCookieJar() {
label: 'Name',
defaultValue: 'My Jar',
});
if (name == null) return null;
return invokeCmd('cmd_create_cookie_jar', { workspaceId: workspace.id, name });
},
onSuccess: (cookieJar) => {
if (cookieJar == null) return;
// Optimistic update
setCookieJars(updateModelList(cookieJar));
},
onSettled: () => trackEvent('cookie_jar', 'create'),
});
}

View File

@@ -1,17 +1,21 @@
import { useMutation } from '@tanstack/react-query';
import type { Environment } from '@yaakapp-internal/models';
import {useSetAtom} from "jotai";
import { trackEvent } from '../lib/analytics';
import { invokeCmd } from '../lib/tauri';
import { useActiveEnvironment } from './useActiveEnvironment';
import { useActiveWorkspace } from './useActiveWorkspace';
import {environmentsAtom} from "./useEnvironments";
import { usePrompt } from './usePrompt';
import {updateModelList} from "./useSyncModelStores";
export function useCreateEnvironment() {
const [, setActiveEnvironmentId] = useActiveEnvironment();
const prompt = usePrompt();
const workspace = useActiveWorkspace();
const setEnvironments = useSetAtom(environmentsAtom);
return useMutation<Environment, unknown, void>({
return useMutation<Environment | null, unknown, void>({
mutationKey: ['create_environment'],
mutationFn: async () => {
const name = await prompt({
@@ -23,6 +27,8 @@ export function useCreateEnvironment() {
defaultValue: 'My Environment',
confirmText: 'Create',
});
if (name == null) return null;
return invokeCmd('cmd_create_environment', {
name,
variables: [],
@@ -31,7 +37,11 @@ export function useCreateEnvironment() {
},
onSettled: () => trackEvent('environment', 'create'),
onSuccess: async (environment) => {
if (workspace == null) return;
if (environment == null) return;
// Optimistic update
setEnvironments(updateModelList(environment));
setActiveEnvironmentId(environment.id);
},
});

View File

@@ -1,15 +1,23 @@
import { useMutation } from '@tanstack/react-query';
import type { Folder } from '@yaakapp-internal/models';
import { useSetAtom } from 'jotai';
import { trackEvent } from '../lib/analytics';
import { invokeCmd } from '../lib/tauri';
import { useActiveWorkspace } from './useActiveWorkspace';
import { foldersAtom } from './useFolders';
import { usePrompt } from './usePrompt';
import { updateModelList } from './useSyncModelStores';
export function useCreateFolder() {
const workspace = useActiveWorkspace();
const prompt = usePrompt();
const setFolders = useSetAtom(foldersAtom);
return useMutation<void, unknown, Partial<Pick<Folder, 'name' | 'sortPriority' | 'folderId'>>>({
return useMutation<
Folder | null,
unknown,
Partial<Pick<Folder, 'name' | 'sortPriority' | 'folderId'>>
>({
mutationKey: ['create_folder'],
mutationFn: async (patch) => {
if (workspace === null) {
@@ -25,14 +33,19 @@ export function useCreateFolder() {
confirmText: 'Create',
placeholder: 'Name',
});
if (name == null) {
return;
}
if (name == null) return null;
patch.name = name;
}
patch.sortPriority = patch.sortPriority || -Date.now();
await invokeCmd('cmd_create_folder', { workspaceId: workspace.id, ...patch });
return await invokeCmd('cmd_create_folder', { workspaceId: workspace.id, ...patch });
},
onSuccess: (folder) => {
if (folder == null) return;
// Optimistic update
setFolders(updateModelList(folder));
},
onSettled: () => trackEvent('folder', 'create'),
});

View File

@@ -1,17 +1,23 @@
import { useMutation } from '@tanstack/react-query';
import type { GrpcRequest } from '@yaakapp-internal/models';
import {useSetAtom} from "jotai";
import { trackEvent } from '../lib/analytics';
import { invokeCmd } from '../lib/tauri';
import {useActiveCookieJar} from "./useActiveCookieJar";
import { useActiveEnvironment } from './useActiveEnvironment';
import { useActiveRequest } from './useActiveRequest';
import { useActiveWorkspace } from './useActiveWorkspace';
import { useAppRoutes } from './useAppRoutes';
import {grpcRequestsAtom} from "./useGrpcRequests";
import {updateModelList} from "./useSyncModelStores";
export function useCreateGrpcRequest() {
const workspace = useActiveWorkspace();
const [activeEnvironment] = useActiveEnvironment();
const [activeCookieJar] = useActiveCookieJar();
const activeRequest = useActiveRequest();
const routes = useAppRoutes();
const setGrpcRequests = useSetAtom(grpcRequestsAtom);
return useMutation<
GrpcRequest,
@@ -19,7 +25,7 @@ export function useCreateGrpcRequest() {
Partial<Pick<GrpcRequest, 'name' | 'sortPriority' | 'folderId'>>
>({
mutationKey: ['create_grpc_request'],
mutationFn: (patch) => {
mutationFn: async (patch) => {
if (workspace === null) {
throw new Error("Cannot create grpc request when there's no active workspace");
}
@@ -33,7 +39,7 @@ export function useCreateGrpcRequest() {
}
}
patch.folderId = patch.folderId || activeRequest?.folderId;
return invokeCmd('cmd_create_grpc_request', {
return invokeCmd<GrpcRequest>('cmd_create_grpc_request', {
workspaceId: workspace.id,
name: '',
...patch,
@@ -41,10 +47,14 @@ export function useCreateGrpcRequest() {
},
onSettled: () => trackEvent('grpc_request', 'create'),
onSuccess: async (request) => {
// Optimistic update
setGrpcRequests(updateModelList(request));
routes.navigate('request', {
workspaceId: request.workspaceId,
requestId: request.id,
environmentId: activeEnvironment?.id,
environmentId: activeEnvironment?.id ?? null,
cookieJarId: activeCookieJar?.id ?? null,
});
},
});

View File

@@ -1,27 +1,33 @@
import { useMutation } from '@tanstack/react-query';
import type { HttpRequest } from '@yaakapp-internal/models';
import { useSetAtom } from 'jotai/index';
import { trackEvent } from '../lib/analytics';
import { invokeCmd } from '../lib/tauri';
import { useActiveCookieJar } from './useActiveCookieJar';
import { useActiveEnvironment } from './useActiveEnvironment';
import { useActiveRequest } from './useActiveRequest';
import { useActiveWorkspace } from './useActiveWorkspace';
import { useAppRoutes } from './useAppRoutes';
import { httpRequestsAtom } from './useHttpRequests';
import { updateModelList } from './useSyncModelStores';
export function useCreateHttpRequest() {
const workspace = useActiveWorkspace();
const [activeEnvironment] = useActiveEnvironment();
const [activeCookieJar] = useActiveCookieJar();
const activeRequest = useActiveRequest();
const routes = useAppRoutes();
const setHttpRequests = useSetAtom(httpRequestsAtom);
return useMutation<HttpRequest, unknown, Partial<HttpRequest>>({
mutationKey: ['create_http_request'],
mutationFn: (patch = {}) => {
mutationFn: async (patch = {}) => {
if (workspace === null) {
throw new Error("Cannot create request when there's no active workspace");
}
if (patch.sortPriority === undefined) {
if (activeRequest != null) {
// Place above currently-active request
// Place above currently active request
patch.sortPriority = activeRequest.sortPriority - 0.0001;
} else {
// Place at the very top
@@ -29,16 +35,20 @@ export function useCreateHttpRequest() {
}
}
patch.folderId = patch.folderId || activeRequest?.folderId;
return invokeCmd('cmd_create_http_request', {
return invokeCmd<HttpRequest>('cmd_create_http_request', {
request: { workspaceId: workspace.id, ...patch },
});
},
onSettled: () => trackEvent('http_request', 'create'),
onSuccess: async (request) => {
// Optimistic update
setHttpRequests(updateModelList(request));
routes.navigate('request', {
workspaceId: request.workspaceId,
requestId: request.id,
environmentId: activeEnvironment?.id,
environmentId: activeEnvironment?.id ?? null,
cookieJarId: activeCookieJar?.id ?? null,
});
},
});

View File

@@ -1,13 +1,18 @@
import { useMutation } from '@tanstack/react-query';
import type { Workspace } from '@yaakapp-internal/models';
import { useSetAtom } from 'jotai/index';
import { invokeCmd } from '../lib/tauri';
import { useAppRoutes } from './useAppRoutes';
import { usePrompt } from './usePrompt';
import { updateModelList } from './useSyncModelStores';
import { workspacesAtom } from './useWorkspaces';
export function useCreateWorkspace() {
const routes = useAppRoutes();
const prompt = usePrompt();
return useMutation<Workspace, void, void>({
const setWorkspaces = useSetAtom(workspacesAtom);
return useMutation<Workspace | null, void, void>({
mutationKey: ['create_workspace'],
mutationFn: async () => {
const name = await prompt({
@@ -18,10 +23,22 @@ export function useCreateWorkspace() {
placeholder: 'My Workspace',
confirmText: 'Create',
});
return invokeCmd('cmd_create_workspace', { name });
if (name == null) {
return null;
}
return invokeCmd<Workspace>('cmd_create_workspace', { name });
},
onSuccess: async (workspace) => {
routes.navigate('workspace', { workspaceId: workspace.id });
if (workspace == null) return;
// Optimistic update
setWorkspaces(updateModelList(workspace));
routes.navigate('workspace', {
workspaceId: workspace.id,
environmentId: null,
cookieJarId: null,
});
},
});
}

View File

@@ -1,14 +1,18 @@
import { useMutation } from '@tanstack/react-query';
import type { GrpcRequest } from '@yaakapp-internal/models';
import {useSetAtom} from "jotai";
import { InlineCode } from '../components/core/InlineCode';
import { trackEvent } from '../lib/analytics';
import { fallbackRequestName } from '../lib/fallbackRequestName';
import { getGrpcRequest } from '../lib/store';
import { invokeCmd } from '../lib/tauri';
import { useConfirm } from './useConfirm';
import {grpcRequestsAtom} from "./useGrpcRequests";
import {removeModelById} from "./useSyncModelStores";
export function useDeleteAnyGrpcRequest() {
const confirm = useConfirm();
const setGrpcRequests = useSetAtom(grpcRequestsAtom);
return useMutation<GrpcRequest | null, string, string>({
mutationKey: ['delete_any_grpc_request'],
@@ -29,6 +33,12 @@ export function useDeleteAnyGrpcRequest() {
if (!confirmed) return null;
return invokeCmd('cmd_delete_grpc_request', { requestId: id });
},
onSuccess: (request) => {
if (request == null) return;
// Optimistic update
setGrpcRequests(removeModelById(request));
},
onSettled: () => trackEvent('grpc_request', 'delete'),
});
}

View File

@@ -1,14 +1,18 @@
import { useMutation } from '@tanstack/react-query';
import type { HttpRequest } from '@yaakapp-internal/models';
import { useSetAtom } from 'jotai';
import { InlineCode } from '../components/core/InlineCode';
import { trackEvent } from '../lib/analytics';
import { fallbackRequestName } from '../lib/fallbackRequestName';
import { getHttpRequest } from '../lib/store';
import { invokeCmd } from '../lib/tauri';
import { useConfirm } from './useConfirm';
import { httpRequestsAtom } from './useHttpRequests';
import { removeModelById } from './useSyncModelStores';
export function useDeleteAnyHttpRequest() {
const confirm = useConfirm();
const setHttpRequests = useSetAtom(httpRequestsAtom);
return useMutation<HttpRequest | null, string, string>({
mutationKey: ['delete_any_http_request'],
@@ -27,7 +31,13 @@ export function useDeleteAnyHttpRequest() {
),
});
if (!confirmed) return null;
return invokeCmd('cmd_delete_http_request', { requestId: id });
return invokeCmd<HttpRequest>('cmd_delete_http_request', { requestId: id });
},
onSuccess: (request) => {
if (request == null) return;
// Optimistic update
setHttpRequests(removeModelById(request));
},
onSettled: () => trackEvent('http_request', 'delete'),
});

View File

@@ -1,12 +1,16 @@
import { useMutation } from '@tanstack/react-query';
import type { CookieJar } from '@yaakapp-internal/models';
import {useSetAtom} from "jotai";
import { InlineCode } from '../components/core/InlineCode';
import { trackEvent } from '../lib/analytics';
import { invokeCmd } from '../lib/tauri';
import { useConfirm } from './useConfirm';
import {cookieJarsAtom} from "./useCookieJars";
import {removeModelById} from "./useSyncModelStores";
export function useDeleteCookieJar(cookieJar: CookieJar | null) {
const confirm = useConfirm();
const setCookieJars = useSetAtom(cookieJarsAtom);
return useMutation<CookieJar | null, string>({
mutationKey: ['delete_cookie_jar', cookieJar?.id],
@@ -25,5 +29,10 @@ export function useDeleteCookieJar(cookieJar: CookieJar | null) {
return invokeCmd('cmd_delete_cookie_jar', { cookieJarId: cookieJar?.id });
},
onSettled: () => trackEvent('cookie_jar', 'delete'),
onSuccess: (cookieJar) => {
if (cookieJar == null) return;
setCookieJars(removeModelById(cookieJar));
}
});
}

View File

@@ -1,12 +1,16 @@
import { useMutation } from '@tanstack/react-query';
import type { Environment } from '@yaakapp-internal/models';
import {useSetAtom} from "jotai";
import { InlineCode } from '../components/core/InlineCode';
import { trackEvent } from '../lib/analytics';
import { invokeCmd } from '../lib/tauri';
import { useConfirm } from './useConfirm';
import {environmentsAtom} from "./useEnvironments";
import {removeModelById} from "./useSyncModelStores";
export function useDeleteEnvironment(environment: Environment | null) {
const confirm = useConfirm();
const setEnvironments = useSetAtom(environmentsAtom);
return useMutation<Environment | null, string>({
mutationKey: ['delete_environment', environment?.id],
@@ -25,5 +29,10 @@ export function useDeleteEnvironment(environment: Environment | null) {
return invokeCmd('cmd_delete_environment', { environmentId: environment?.id });
},
onSettled: () => trackEvent('environment', 'delete'),
onSuccess: (environment) => {
if (environment == null) return;
setEnvironments(removeModelById(environment));
}
});
}

View File

@@ -1,13 +1,17 @@
import { useMutation } from '@tanstack/react-query';
import type { Folder } from '@yaakapp-internal/models';
import { useSetAtom } from 'jotai';
import { InlineCode } from '../components/core/InlineCode';
import { trackEvent } from '../lib/analytics';
import { getFolder } from '../lib/store';
import { invokeCmd } from '../lib/tauri';
import { useConfirm } from './useConfirm';
import { foldersAtom } from './useFolders';
import { removeModelById } from './useSyncModelStores';
export function useDeleteFolder(id: string | null) {
const confirm = useConfirm();
const setFolders = useSetAtom(foldersAtom);
return useMutation<Folder | null, string>({
mutationKey: ['delete_folder', id],
@@ -27,5 +31,10 @@ export function useDeleteFolder(id: string | null) {
return invokeCmd('cmd_delete_folder', { folderId: id });
},
onSettled: () => trackEvent('folder', 'delete'),
onSuccess: (folder) => {
if (folder == null) return;
setFolders(removeModelById(folder));
},
});
}

View File

@@ -1,14 +1,23 @@
import { useMutation } from '@tanstack/react-query';
import type { GrpcConnection } from '@yaakapp-internal/models';
import {useSetAtom} from "jotai";
import { trackEvent } from '../lib/analytics';
import { invokeCmd } from '../lib/tauri';
import {grpcConnectionsAtom} from "./useGrpcConnections";
import {removeModelById} from "./useSyncModelStores";
export function useDeleteGrpcConnection(id: string | null) {
const setGrpcConnections = useSetAtom(grpcConnectionsAtom);
return useMutation<GrpcConnection>({
mutationKey: ['delete_grpc_connection', id],
mutationFn: async () => {
return await invokeCmd('cmd_delete_grpc_connection', { id: id });
},
onSettled: () => trackEvent('grpc_connection', 'delete'),
onSuccess: (connection) => {
if (connection == null) return;
setGrpcConnections(removeModelById(connection));
}
});
}

View File

@@ -1,8 +1,11 @@
import { useMutation } from '@tanstack/react-query';
import { useSetAtom } from 'jotai';
import { trackEvent } from '../lib/analytics';
import { invokeCmd } from '../lib/tauri';
import { grpcConnectionsAtom } from './useGrpcConnections';
export function useDeleteGrpcConnections(requestId?: string) {
const setGrpcConnections = useSetAtom(grpcConnectionsAtom);
return useMutation({
mutationKey: ['delete_grpc_connections', requestId],
mutationFn: async () => {
@@ -10,5 +13,8 @@ export function useDeleteGrpcConnections(requestId?: string) {
await invokeCmd('cmd_delete_all_grpc_connections', { requestId });
},
onSettled: () => trackEvent('grpc_connection', 'delete_many'),
onSuccess: () => {
setGrpcConnections((all) => all.filter((r) => r.requestId !== requestId));
},
});
}

View File

@@ -1,14 +1,21 @@
import { useMutation } from '@tanstack/react-query';
import type { HttpResponse } from '@yaakapp-internal/models';
import {useSetAtom} from "jotai";
import { trackEvent } from '../lib/analytics';
import { invokeCmd } from '../lib/tauri';
import {httpResponsesAtom} from "./useHttpResponses";
import {removeModelById} from "./useSyncModelStores";
export function useDeleteHttpResponse(id: string | null) {
const setHttpResponses = useSetAtom(httpResponsesAtom);
return useMutation<HttpResponse>({
mutationKey: ['delete_http_response', id],
mutationFn: async () => {
return await invokeCmd('cmd_delete_http_response', { id: id });
},
onSettled: () => trackEvent('http_response', 'delete'),
onSuccess: (response) => {
setHttpResponses(removeModelById(response));
}
});
}

View File

@@ -1,14 +1,20 @@
import { useMutation } from '@tanstack/react-query';
import { useSetAtom } from 'jotai';
import { trackEvent } from '../lib/analytics';
import { invokeCmd } from '../lib/tauri';
import { httpResponsesAtom } from './useHttpResponses';
export function useDeleteHttpResponses(requestId?: string) {
const setHttpResponses = useSetAtom(httpResponsesAtom);
return useMutation({
mutationKey: ['delete_http_responses', requestId],
mutationFn: async () => {
if (requestId === undefined) return;
await invokeCmd('cmd_delete_all_http_responses', { requestId });
},
onSuccess: () => {
setHttpResponses((all) => all.filter((r) => r.requestId !== requestId));
},
onSettled: () => trackEvent('http_response', 'delete_many'),
});
}

View File

@@ -0,0 +1,52 @@
import { useMutation } from '@tanstack/react-query';
import { useSetAtom } from 'jotai/index';
import { count } from '../lib/pluralize';
import { invokeCmd } from '../lib/tauri';
import { useActiveWorkspace } from './useActiveWorkspace';
import { useAlert } from './useAlert';
import { useConfirm } from './useConfirm';
import { useGrpcConnections } from './useGrpcConnections';
import { httpResponsesAtom, useHttpResponses } from './useHttpResponses';
export function useDeleteSendHistory() {
const confirm = useConfirm();
const alert = useAlert();
const setHttpResponses = useSetAtom(httpResponsesAtom);
const activeWorkspace = useActiveWorkspace();
const httpResponses = useHttpResponses();
const grpcConnections = useGrpcConnections();
const labels = [
httpResponses.length > 0 ? count('Http Response', httpResponses.length) : null,
grpcConnections.length > 0 ? count('Grpc Connection', grpcConnections.length) : null,
].filter((l) => l != null);
return useMutation({
mutationKey: ['delete_send_history'],
mutationFn: async () => {
if (labels.length === 0) {
alert({
id: 'no-responses',
title: 'Nothing to Delete',
body: 'There are no Http Response or Grpc Connections to delete',
});
return;
}
const confirmed = await confirm({
id: 'delete-send-history',
title: 'Clear Send History',
variant: 'delete',
description: <>Delete {labels.join(' and ')}?</>,
});
if (!confirmed) return false;
await invokeCmd('cmd_delete_send_history', { workspaceId: activeWorkspace?.id ?? 'n/a' });
return true;
},
onSuccess: async (confirmed) => {
if (!confirmed) return;
setHttpResponses((all) => all.filter((r) => r.workspaceId !== activeWorkspace?.id));
},
});
}

View File

@@ -1,16 +1,20 @@
import { useMutation } from '@tanstack/react-query';
import type { Workspace } from '@yaakapp-internal/models';
import {useSetAtom} from "jotai";
import { InlineCode } from '../components/core/InlineCode';
import { trackEvent } from '../lib/analytics';
import { invokeCmd } from '../lib/tauri';
import { useActiveWorkspace } from './useActiveWorkspace';
import { useAppRoutes } from './useAppRoutes';
import { useConfirm } from './useConfirm';
import {removeModelById} from "./useSyncModelStores";
import {workspacesAtom} from "./useWorkspaces";
export function useDeleteWorkspace(workspace: Workspace | null) {
const activeWorkspace = useActiveWorkspace();
const routes = useAppRoutes();
const confirm = useConfirm();
const setWorkspaces = useSetAtom(workspacesAtom);
return useMutation<Workspace | null, string>({
mutationKey: ['delete_workspace', workspace?.id],
@@ -32,6 +36,9 @@ export function useDeleteWorkspace(workspace: Workspace | null) {
onSuccess: async (workspace) => {
if (workspace === null) return;
// Optimistic update
setWorkspaces(removeModelById(workspace));
const { id: workspaceId } = workspace;
if (workspaceId === activeWorkspace?.id) {
routes.navigate('workspaces');

View File

@@ -2,6 +2,7 @@ import { useMutation } from '@tanstack/react-query';
import type { GrpcRequest } from '@yaakapp-internal/models';
import { trackEvent } from '../lib/analytics';
import { invokeCmd } from '../lib/tauri';
import {useActiveCookieJar} from "./useActiveCookieJar";
import { useActiveEnvironment } from './useActiveEnvironment';
import { useActiveWorkspace } from './useActiveWorkspace';
import { useAppRoutes } from './useAppRoutes';
@@ -16,6 +17,7 @@ export function useDuplicateGrpcRequest({
}) {
const activeWorkspace = useActiveWorkspace();
const [activeEnvironment] = useActiveEnvironment();
const [activeCookieJar] = useActiveCookieJar();
const routes = useAppRoutes();
return useMutation<GrpcRequest, string>({
@@ -36,7 +38,8 @@ export function useDuplicateGrpcRequest({
routes.navigate('request', {
workspaceId: activeWorkspace.id,
requestId: request.id,
environmentId: activeEnvironment?.id,
environmentId: activeEnvironment?.id ?? null,
cookieJarId: activeCookieJar?.id ?? null,
});
}
},

View File

@@ -2,6 +2,7 @@ import { useMutation } from '@tanstack/react-query';
import type { HttpRequest } from '@yaakapp-internal/models';
import { trackEvent } from '../lib/analytics';
import { invokeCmd } from '../lib/tauri';
import {useActiveCookieJar} from "./useActiveCookieJar";
import { useActiveEnvironment } from './useActiveEnvironment';
import { useActiveWorkspace } from './useActiveWorkspace';
import { useAppRoutes } from './useAppRoutes';
@@ -15,6 +16,7 @@ export function useDuplicateHttpRequest({
}) {
const activeWorkspace = useActiveWorkspace();
const [activeEnvironment] = useActiveEnvironment();
const [activeCookieJar] = useActiveCookieJar();
const routes = useAppRoutes();
return useMutation<HttpRequest, string>({
mutationKey: ['duplicate_http_request', id],
@@ -28,7 +30,8 @@ export function useDuplicateHttpRequest({
routes.navigate('request', {
workspaceId: activeWorkspace.id,
requestId: request.id,
environmentId: activeEnvironment?.id,
environmentId: activeEnvironment?.id ?? null,
cookieJarId: activeCookieJar?.id ?? null,
});
}
},

View File

@@ -0,0 +1,28 @@
import { useQuery } from '@tanstack/react-query';
import type { EditorProps } from '../components/core/Editor';
import { tryFormatJson, tryFormatXml } from '../lib/formatters';
export function useFormatText({
text,
language,
pretty,
}: {
text: string;
language: EditorProps['language'];
pretty: boolean;
}) {
return useQuery({
queryKey: [text, language, pretty],
queryFn: async () => {
if (text === '' || !pretty) {
return text;
} else if (language === 'json') {
return tryFormatJson(text);
} else if (language === 'xml' || language === 'html') {
return tryFormatXml(text);
} else {
return text;
}
},
});
}

View File

@@ -3,6 +3,7 @@ import { emit } from '@tauri-apps/api/event';
import type { GrpcConnection, GrpcRequest } from '@yaakapp-internal/models';
import { trackEvent } from '../lib/analytics';
import { minPromiseMillis } from '../lib/minPromiseMillis';
import { isResponseLoading } from '../lib/model_util';
import { invokeCmd } from '../lib/tauri';
import { useActiveEnvironment } from './useActiveEnvironment';
import { useDebouncedValue } from './useDebouncedValue';
@@ -22,27 +23,27 @@ export function useGrpc(
const go = useMutation<void, string>({
mutationKey: ['grpc_go', conn?.id],
mutationFn: async () =>
await invokeCmd('cmd_grpc_go', { requestId, environmentId: environment?.id, protoFiles }),
mutationFn: () =>
invokeCmd<void>('cmd_grpc_go', { requestId, environmentId: environment?.id, protoFiles }),
onSettled: () => trackEvent('grpc_request', 'send'),
});
const send = useMutation({
mutationKey: ['grpc_send', conn?.id],
mutationFn: async ({ message }: { message: string }) =>
await emit(`grpc_client_msg_${conn?.id ?? 'none'}`, { Message: message }),
mutationFn: ({ message }: { message: string }) =>
emit(`grpc_client_msg_${conn?.id ?? 'none'}`, { Message: message }),
onSettled: () => trackEvent('grpc_connection', 'send'),
});
const cancel = useMutation({
mutationKey: ['grpc_cancel', conn?.id ?? 'n/a'],
mutationFn: async () => await emit(`grpc_client_msg_${conn?.id ?? 'none'}`, 'Cancel'),
mutationFn: () => emit(`grpc_client_msg_${conn?.id ?? 'none'}`, 'Cancel'),
onSettled: () => trackEvent('grpc_connection', 'cancel'),
});
const commit = useMutation({
mutationKey: ['grpc_commit', conn?.id ?? 'n/a'],
mutationFn: async () => await emit(`grpc_client_msg_${conn?.id ?? 'none'}`, 'Commit'),
mutationFn: () => emit(`grpc_client_msg_${conn?.id ?? 'none'}`, 'Commit'),
onSettled: () => trackEvent('grpc_connection', 'commit'),
});
@@ -51,11 +52,11 @@ export function useGrpc(
const reflect = useQuery<ReflectResponseService[], string>({
enabled: req != null,
queryKey: ['grpc_reflect', req?.id ?? 'n/a', debouncedUrl, protoFiles],
queryFn: async () =>
(await minPromiseMillis(
queryFn: () =>
minPromiseMillis<ReflectResponseService[]>(
invokeCmd('cmd_grpc_reflect', { requestId, protoFiles }),
300,
)) as ReflectResponseService[],
),
});
return {
@@ -63,7 +64,7 @@ export function useGrpc(
reflect,
cancel,
commit,
isStreaming: conn != null && conn.elapsed === 0,
isStreaming: isResponseLoading(conn),
send,
};
}

View File

@@ -1,4 +1,5 @@
import { useMutation } from '@tanstack/react-query';
import type { HttpRequest } from '@yaakapp-internal/models';
import { useToast } from '../components/ToastContext';
import { invokeCmd } from '../lib/tauri';
import { useActiveWorkspace } from './useActiveWorkspace';
@@ -22,11 +23,10 @@ export function useImportCurl() {
overwriteRequestId?: string;
command: string;
}) => {
const request: Record<string, unknown> = await invokeCmd('cmd_curl_to_request', {
const request: HttpRequest = await invokeCmd('cmd_curl_to_request', {
command,
workspaceId: workspace?.id,
});
delete request.id;
let verb;
if (overwriteRequestId == null) {
@@ -34,7 +34,19 @@ export function useImportCurl() {
await createRequest.mutateAsync(request);
} else {
verb = 'Updated';
await updateRequest.mutateAsync({ id: overwriteRequestId, update: request });
await updateRequest.mutateAsync({
id: overwriteRequestId,
update: (r: HttpRequest) => ({
...request,
id: r.id,
createdAt: r.createdAt,
workspaceId: r.workspaceId,
folderId: r.folderId,
name: r.name,
sortPriority: r.sortPriority,
}),
});
setTimeout(() => wasUpdatedExternally(overwriteRequestId), 100);
}

View File

@@ -66,7 +66,8 @@ export function useImportData() {
if (importedWorkspace != null) {
routes.navigate('workspace', {
workspaceId: importedWorkspace.id,
environmentId: imported.environments[0]?.id,
environmentId: imported.environments[0]?.id ?? null,
cookieJarId: null,
});
}

View File

@@ -14,12 +14,14 @@ const introspectionRequestBody = JSON.stringify({
operationName: 'IntrospectionQuery',
});
export function useIntrospectGraphQL(baseRequest: HttpRequest) {
export function useIntrospectGraphQL(
baseRequest: HttpRequest,
options: { disabled?: boolean } = {},
) {
// Debounce the request because it can change rapidly and we don't
// want to send so too many requests.
const request = useDebouncedValue(baseRequest);
const [activeEnvironment] = useActiveEnvironment();
const [refetchKey, setRefetchKey] = useState<number>(0);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [error, setError] = useState<string>();
@@ -29,10 +31,11 @@ export function useIntrospectGraphQL(baseRequest: HttpRequest) {
namespace: 'global',
});
useEffect(() => {
const fetchIntrospection = async () => {
const refetch = useCallback(async () => {
try {
setIsLoading(true);
setError(undefined);
const args = {
...baseRequest,
bodyType: 'application/json',
@@ -44,42 +47,54 @@ export function useIntrospectGraphQL(baseRequest: HttpRequest) {
);
if (response.error) {
throw new Error(response.error);
return setError(response.error);
}
const bodyText = await getResponseBodyText(response);
if (response.status < 200 || response.status >= 300) {
throw new Error(`Request failed with status ${response.status}.\n\n${bodyText}`);
return setError(`Request failed with status ${response.status}.\n\n${bodyText}`);
}
if (bodyText === null) {
throw new Error('Empty body returned in response');
return setError('Empty body returned in response');
}
const { data } = JSON.parse(bodyText);
console.log(`Got introspection response for ${baseRequest.url}`, data);
await setIntrospection(data);
};
} catch (err) {
setError(String(err));
} finally {
setIsLoading(false);
}
}, [activeEnvironment?.id, baseRequest, setIntrospection]);
fetchIntrospection()
.catch((e) => setError(e.message))
.finally(() => setIsLoading(false));
useEffect(() => {
// Skip introspection if automatic is disabled and we already have one
if (options.disabled) {
return;
}
refetch().catch(console.error);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [request.id, request.url, request.method, refetchKey, activeEnvironment?.id]);
}, [request.id, request.url, request.method, activeEnvironment?.id]);
const refetch = useCallback(() => {
setRefetchKey((k) => k + 1);
}, []);
const clear = useCallback(async () => {
await setIntrospection(null);
}, [setIntrospection]);
const schema = useMemo(() => {
if (introspection == null) {
return introspection;
}
try {
return introspection ? buildClientSchema(introspection) : undefined;
return buildClientSchema(introspection);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (e: any) {
setError('message' in e ? e.message : String(e));
}
}, [introspection]);
return { schema, isLoading, error, refetch };
return { schema, isLoading, error, refetch, clear };
}

View File

@@ -12,7 +12,11 @@ export function useOpenSettings() {
if (workspace == null) return;
await invokeCmd('cmd_new_child_window', {
url: routes.paths.workspaceSettings({ workspaceId: workspace.id }),
url: routes.paths.workspaceSettings({
workspaceId: workspace.id,
cookieJarId: null,
environmentId: null,
}),
label: 'settings',
title: 'Yaak Settings',
innerSize: [600, 550],

View File

@@ -17,9 +17,9 @@ export function useOpenWorkspace() {
workspaceId: string;
inNewWindow: boolean;
}) => {
const environmentId = (await getRecentEnvironments(workspaceId))[0];
const requestId = (await getRecentRequests(workspaceId))[0];
const cookieJarId = (await getRecentCookieJars(workspaceId))[0];
const environmentId = (await getRecentEnvironments(workspaceId))[0] ?? null;
const requestId = (await getRecentRequests(workspaceId))[0] ?? null;
const cookieJarId = (await getRecentCookieJars(workspaceId))[0] ?? null;
const baseArgs = { workspaceId, environmentId, cookieJarId } as const;
if (inNewWindow) {
const path =

View File

@@ -4,6 +4,7 @@ import type { AnyModel } from '@yaakapp-internal/models';
import { useSetAtom } from 'jotai/index';
import { extractKeyValue } from '../lib/keyValueStore';
import { modelsEq } from '../lib/model_util';
import { useActiveWorkspace } from './useActiveWorkspace';
import { cookieJarsAtom } from './useCookieJars';
import { environmentsAtom } from './useEnvironments';
import { foldersAtom } from './useFolders';
@@ -25,6 +26,7 @@ export interface ModelPayload {
}
export function useSyncModelStores() {
const activeWorkspace = useActiveWorkspace();
const queryClient = useQueryClient();
const { wasUpdatedExternally } = useRequestUpdateKey(null);
@@ -48,34 +50,36 @@ export function useSyncModelStores() {
? keyValueQueryKey(model)
: null;
// TODO: Move this logic to useRequestEditor() hook
if (model.model === 'http_request' && windowLabel !== getCurrentWebviewWindow().label) {
wasUpdatedExternally(model.id);
}
const pushToFront = (['http_response', 'grpc_connection'] as AnyModel['model'][]).includes(
model.model,
);
// Only sync models that belong to this workspace, if a workspace ID is present
if ('workspaceId' in model && model.workspaceId !== activeWorkspace?.id) {
return;
}
if (shouldIgnoreModel(model, windowLabel)) return;
if (model.model === 'workspace') {
setWorkspaces(updateModelList(model, pushToFront));
setWorkspaces(updateModelList(model));
} else if (model.model === 'plugin') {
setPlugins(updateModelList(model, pushToFront));
setPlugins(updateModelList(model));
} else if (model.model === 'http_request') {
setHttpRequests(updateModelList(model, pushToFront));
setHttpRequests(updateModelList(model));
} else if (model.model === 'folder') {
setFolders(updateModelList(model, pushToFront));
setFolders(updateModelList(model));
} else if (model.model === 'http_response') {
setHttpResponses(updateModelList(model, pushToFront));
setHttpResponses(updateModelList(model));
} else if (model.model === 'grpc_request') {
setGrpcRequests(updateModelList(model, pushToFront));
setGrpcRequests(updateModelList(model));
} else if (model.model === 'grpc_connection') {
setGrpcConnections(updateModelList(model, pushToFront));
setGrpcConnections(updateModelList(model));
} else if (model.model === 'environment') {
setEnvironments(updateModelList(model, pushToFront));
setEnvironments(updateModelList(model));
} else if (model.model === 'cookie_jar') {
setCookieJars(updateModelList(model, pushToFront));
setCookieJars(updateModelList(model));
} else if (model.model === 'settings') {
setSettings(model);
} else if (queryKey != null) {
@@ -87,7 +91,7 @@ export function useSyncModelStores() {
}
if (Array.isArray(current)) {
return updateModelList(model, pushToFront)(current);
return updateModelList(model)(current);
}
});
}
@@ -97,35 +101,38 @@ export function useSyncModelStores() {
const { model, windowLabel } = payload;
if (shouldIgnoreModel(model, windowLabel)) return;
console.log('Delete model', payload.model);
console.log('Delete model', payload);
if (model.model === 'workspace') {
setWorkspaces(removeById(model));
setWorkspaces(removeModelById(model));
} else if (model.model === 'plugin') {
setPlugins(removeById(model));
setPlugins(removeModelById(model));
} else if (model.model === 'http_request') {
setHttpRequests(removeById(model));
setHttpRequests(removeModelById(model));
} else if (model.model === 'http_response') {
setHttpResponses(removeById(model));
setHttpResponses(removeModelById(model));
} else if (model.model === 'folder') {
setFolders(removeById(model));
setFolders(removeModelById(model));
} else if (model.model === 'environment') {
setEnvironments(removeById(model));
setEnvironments(removeModelById(model));
} else if (model.model === 'grpc_request') {
setGrpcRequests(removeById(model));
setGrpcRequests(removeModelById(model));
} else if (model.model === 'grpc_connection') {
setGrpcConnections(removeById(model));
setGrpcConnections(removeModelById(model));
} else if (model.model === 'grpc_event') {
queryClient.setQueryData(grpcEventsQueryKey(model), removeById(model));
queryClient.setQueryData(grpcEventsQueryKey(model), removeModelById(model));
} else if (model.model === 'key_value') {
queryClient.setQueryData(keyValueQueryKey(model), undefined);
} else if (model.model === 'cookie_jar') {
setCookieJars(removeById(model));
setCookieJars(removeModelById(model));
}
});
}
function updateModelList<T extends AnyModel>(model: T, pushToFront: boolean) {
export function updateModelList<T extends AnyModel>(model: T) {
// Mark these models as DESC instead of ASC
const pushToFront = model.model === 'http_response' || model.model === 'grpc_connection';
return (current: T[] | undefined): T[] => {
const index = current?.findIndex((v) => modelsEq(v, model)) ?? -1;
if (index >= 0) {
@@ -136,7 +143,7 @@ function updateModelList<T extends AnyModel>(model: T, pushToFront: boolean) {
};
}
function removeById<T extends { id: string }>(model: T) {
export function removeModelById<T extends { id: string }>(model: T) {
return (entries: T[] | undefined) => entries?.filter((e) => e.id !== model.id) ?? [];
}

View File

@@ -23,6 +23,7 @@ export function useSyncWorkspaceChildModels() {
const workspaceId = workspace?.id ?? 'n/a';
useEffect(() => {
(async function () {
console.log('Syncing model stores', { workspaceId });
// Set the things we need first, first
setHttpRequests(await invokeCmd('cmd_list_http_requests', { workspaceId }));
setGrpcRequests(await invokeCmd('cmd_list_grpc_requests', { workspaceId }));

View File

@@ -1,18 +1,25 @@
import { useQuery } from '@tanstack/react-query';
import type { GetTemplateFunctionsResponse } from '@yaakapp-internal/plugin';
import { useState } from 'react';
import { invokeCmd } from '../lib/tauri';
import { usePluginsKey } from './usePlugins';
export function useTemplateFunctions() {
const pluginsKey = usePluginsKey();
const [numFns, setNumFns] = useState<number>(0);
const result = useQuery({
queryKey: ['template_functions', pluginsKey],
// Fetch periodically until functions are returned
// NOTE: visibilitychange (refetchOnWindowFocus) does not work on Windows, so we'll rely on this logic
// to refetch things until that's working again
// TODO: Update plugin system to wait for plugins to initialize before sending the first event to them
refetchInterval: numFns > 0 ? Infinity : 500,
refetchOnMount: true,
queryFn: async () => {
const responses = (await invokeCmd(
'cmd_template_functions',
)) as GetTemplateFunctionsResponse[];
return responses;
const result = await invokeCmd<GetTemplateFunctionsResponse[]>('cmd_template_functions');
setNumFns(result.length);
return result;
},
});

View File

@@ -1,10 +1,14 @@
import { useMutation } from '@tanstack/react-query';
import type { Folder } from '@yaakapp-internal/models';
import {useSetAtom} from "jotai/index";
import { getFolder } from '../lib/store';
import { invokeCmd } from '../lib/tauri';
import {foldersAtom} from "./useFolders";
import {updateModelList} from "./useSyncModelStores";
export function useUpdateAnyFolder() {
return useMutation<void, unknown, { id: string; update: (r: Folder) => Folder }>({
const setFolders = useSetAtom(foldersAtom);
return useMutation<Folder, unknown, { id: string; update: (r: Folder) => Folder }>({
mutationKey: ['update_any_folder'],
mutationFn: async ({ id, update }) => {
const folder = await getFolder(id);
@@ -12,7 +16,10 @@ export function useUpdateAnyFolder() {
throw new Error("Can't update a null folder");
}
await invokeCmd('cmd_update_folder', { folder: update(folder) });
return invokeCmd<Folder>('cmd_update_folder', { folder: update(folder) });
},
onSuccess: async (folder) => {
setFolders(updateModelList(folder));
}
});
}

View File

@@ -1,11 +1,15 @@
import { useMutation } from '@tanstack/react-query';
import type { GrpcRequest } from '@yaakapp-internal/models';
import { useSetAtom } from 'jotai/index';
import { getGrpcRequest } from '../lib/store';
import { invokeCmd } from '../lib/tauri';
import { grpcRequestsAtom } from './useGrpcRequests';
import { updateModelList } from './useSyncModelStores';
export function useUpdateAnyGrpcRequest() {
const setGrpcRequests = useSetAtom(grpcRequestsAtom);
return useMutation<
void,
GrpcRequest,
unknown,
{ id: string; update: Partial<GrpcRequest> | ((r: GrpcRequest) => GrpcRequest) }
>({
@@ -18,7 +22,10 @@ export function useUpdateAnyGrpcRequest() {
const patchedRequest =
typeof update === 'function' ? update(request) : { ...request, ...update };
await invokeCmd('cmd_update_grpc_request', { request: patchedRequest });
return invokeCmd<GrpcRequest>('cmd_update_grpc_request', { request: patchedRequest });
},
onSuccess: (request) => {
setGrpcRequests(updateModelList(request));
},
});
}

View File

@@ -1,11 +1,15 @@
import { useMutation } from '@tanstack/react-query';
import type { HttpRequest } from '@yaakapp-internal/models';
import {useSetAtom} from "jotai/index";
import { getHttpRequest } from '../lib/store';
import { invokeCmd } from '../lib/tauri';
import {httpRequestsAtom} from "./useHttpRequests";
import {updateModelList} from "./useSyncModelStores";
export function useUpdateAnyHttpRequest() {
const setHttpRequests = useSetAtom(httpRequestsAtom);
return useMutation<
void,
HttpRequest,
unknown,
{ id: string; update: Partial<HttpRequest> | ((r: HttpRequest) => HttpRequest) }
>({
@@ -18,7 +22,10 @@ export function useUpdateAnyHttpRequest() {
const patchedRequest =
typeof update === 'function' ? update(request) : { ...request, ...update };
await invokeCmd('cmd_update_http_request', { request: patchedRequest });
return invokeCmd<HttpRequest>('cmd_update_http_request', { request: patchedRequest });
},
onSuccess: async (request) => {
setHttpRequests(updateModelList(request));
}
});
}

View File

@@ -1,10 +1,14 @@
import { useMutation } from '@tanstack/react-query';
import type { CookieJar } from '@yaakapp-internal/models';
import { useSetAtom } from 'jotai/index';
import { getCookieJar } from '../lib/store';
import { invokeCmd } from '../lib/tauri';
import { cookieJarsAtom } from './useCookieJars';
import { updateModelList } from './useSyncModelStores';
export function useUpdateCookieJar(id: string | null) {
return useMutation<void, unknown, Partial<CookieJar> | ((j: CookieJar) => CookieJar)>({
const setCookieJars = useSetAtom(cookieJarsAtom);
return useMutation<CookieJar, unknown, Partial<CookieJar> | ((j: CookieJar) => CookieJar)>({
mutationKey: ['update_cookie_jar', id],
mutationFn: async (v) => {
const cookieJar = await getCookieJar(id);
@@ -13,7 +17,10 @@ export function useUpdateCookieJar(id: string | null) {
}
const newCookieJar = typeof v === 'function' ? v(cookieJar) : { ...cookieJar, ...v };
await invokeCmd('cmd_update_cookie_jar', { cookieJar: newCookieJar });
return invokeCmd<CookieJar>('cmd_update_cookie_jar', { cookieJar: newCookieJar });
},
onSuccess: (cookieJar) => {
setCookieJars(updateModelList(cookieJar));
},
});
}

View File

@@ -1,10 +1,18 @@
import { useMutation } from '@tanstack/react-query';
import type { Environment } from '@yaakapp-internal/models';
import { useSetAtom } from 'jotai/index';
import { getEnvironment } from '../lib/store';
import { invokeCmd } from '../lib/tauri';
import { environmentsAtom } from './useEnvironments';
import {updateModelList} from "./useSyncModelStores";
export function useUpdateEnvironment(id: string | null) {
return useMutation<void, unknown, Partial<Environment> | ((r: Environment) => Environment)>({
const setEnvironments = useSetAtom(environmentsAtom);
return useMutation<
Environment,
unknown,
Partial<Environment> | ((r: Environment) => Environment)
>({
mutationKey: ['update_environment', id],
mutationFn: async (v) => {
const environment = await getEnvironment(id);
@@ -13,7 +21,10 @@ export function useUpdateEnvironment(id: string | null) {
}
const newEnvironment = typeof v === 'function' ? v(environment) : { ...environment, ...v };
await invokeCmd('cmd_update_environment', { environment: newEnvironment });
return invokeCmd<Environment>('cmd_update_environment', { environment: newEnvironment });
},
onSuccess: async (environment) => {
setEnvironments(updateModelList(environment));
},
});
}

View File

@@ -1,15 +1,21 @@
import { useMutation } from '@tanstack/react-query';
import type { Settings } from '@yaakapp-internal/models';
import { useSetAtom } from 'jotai';
import { getSettings } from '../lib/store';
import { invokeCmd } from '../lib/tauri';
import { settingsAtom } from './useSettings';
export function useUpdateSettings() {
return useMutation<void, unknown, Partial<Settings>>({
const setSettings = useSetAtom(settingsAtom);
return useMutation<Settings, unknown, Partial<Settings>>({
mutationKey: ['update_settings'],
mutationFn: async (patch) => {
const settings = await getSettings();
const newSettings: Settings = { ...settings, ...patch };
await invokeCmd('cmd_update_settings', { settings: newSettings });
return invokeCmd<Settings>('cmd_update_settings', { settings: newSettings });
},
onSuccess: (settings) => {
setSettings(settings);
},
});
}

View File

@@ -1,10 +1,14 @@
import { useMutation } from '@tanstack/react-query';
import type { Workspace } from '@yaakapp-internal/models';
import {useSetAtom} from "jotai/index";
import { getWorkspace } from '../lib/store';
import { invokeCmd } from '../lib/tauri';
import {updateModelList} from "./useSyncModelStores";
import {workspacesAtom} from "./useWorkspaces";
export function useUpdateWorkspace(id: string | null) {
return useMutation<void, unknown, Partial<Workspace> | ((w: Workspace) => Workspace)>({
const setWorkspaces = useSetAtom(workspacesAtom);
return useMutation<Workspace, unknown, Partial<Workspace> | ((w: Workspace) => Workspace)>({
mutationKey: ['update_workspace', id],
mutationFn: async (v) => {
const workspace = await getWorkspace(id);
@@ -13,7 +17,10 @@ export function useUpdateWorkspace(id: string | null) {
}
const newWorkspace = typeof v === 'function' ? v(workspace) : { ...workspace, ...v };
await invokeCmd('cmd_update_workspace', { workspace: newWorkspace });
return invokeCmd('cmd_update_workspace', { workspace: newWorkspace });
},
onSuccess: async (workspace) => {
setWorkspaces(updateModelList(workspace));
},
});
}

View File

@@ -1,20 +1,31 @@
import xmlFormat from 'xml-formatter';
import { invokeCmd } from './tauri';
const INDENT = ' ';
export function tryFormatJson(text: string, pretty = true): string {
export async function tryFormatJson(text: string): Promise<string> {
if (text === '') return text;
try {
if (pretty) return JSON.stringify(JSON.parse(text), null, INDENT);
else return JSON.stringify(JSON.parse(text));
const result = await invokeCmd<string>('cmd_format_json', { text });
return result;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (err) {
return text;
console.warn("Failed to format JSON", err);
// Nothing
}
try {
return JSON.stringify(JSON.parse(text), null, 2);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (err) {
// Nothing
}
return text;
}
export function tryFormatXml(text: string): string {
export async function tryFormatXml(text: string): Promise<string> {
if (text === '') return text;
try {

View File

@@ -5,6 +5,7 @@ import type {
HttpResponse,
HttpResponseHeader,
} from '@yaakapp-internal/models';
import MimeType from 'whatwg-mimetype';
export const BODY_TYPE_NONE = null;
export const BODY_TYPE_GRAPHQL = 'graphql';
@@ -58,5 +59,8 @@ export function getContentTypeHeader(headers: HttpResponseHeader[]): string | nu
export function getCharsetFromContentType(headers: HttpResponseHeader[]): string | null {
const contentType = getContentTypeHeader(headers);
return contentType?.match(/charset=([^ ;]+)/)?.[1] ?? null;
if (contentType == null) return null;
const mimeType = new MimeType(contentType);
return mimeType.parameters.get('charset') ?? null;
}

View File

@@ -14,6 +14,7 @@ type TauriCmd =
| 'cmd_curl_to_request'
| 'cmd_delete_all_grpc_connections'
| 'cmd_delete_all_http_responses'
| 'cmd_delete_send_history'
| 'cmd_delete_cookie_jar'
| 'cmd_delete_environment'
| 'cmd_delete_folder'
@@ -27,6 +28,7 @@ type TauriCmd =
| 'cmd_duplicate_http_request'
| 'cmd_export_data'
| 'cmd_filter_response'
| 'cmd_format_json'
| 'cmd_get_cookie_jar'
| 'cmd_get_environment'
| 'cmd_get_folder'

View File

@@ -19,7 +19,7 @@
"@lezer/lr": "^1.3.3",
"@react-hook/resize-observer": "^2.0.2",
"@tailwindcss/container-queries": "^0.1.1",
"@tanstack/react-query": "^5.55.4",
"@tanstack/react-query": "^5.59.16",
"@tanstack/react-virtual": "^3.10.8",
"@tauri-apps/api": "^2.0.1",
"@tauri-apps/plugin-clipboard-manager": "^2.0.0",
@@ -54,6 +54,7 @@
"react-use": "^17.5.1",
"slugify": "^1.6.6",
"uuid": "^10.0.0",
"whatwg-mimetype": "^4.0.0",
"xml-formatter": "^3.6.3"
},
"devDependencies": {
@@ -67,6 +68,7 @@
"@types/react": "^18.3.5",
"@types/react-dom": "^18.3.0",
"@types/uuid": "^10.0.0",
"@types/whatwg-mimetype": "^3.0.2",
"@vitejs/plugin-react": "^4.3.1",
"autoprefixer": "^10.4.20",
"decompress": "^4.2.1",