Compare commits

..

44 Commits

Author SHA1 Message Date
Gregory Schier
eb1916b773 Fix tests 2025-10-24 15:22:20 -07:00
Gregory Schier
a3df0489b1 Fix Insomnia v4 environment importer 2025-10-24 15:21:20 -07:00
Gregory Schier
b19e036a61 Better CSS 2025-10-24 15:06:08 -07:00
Gregory Schier
b51e37f221 Try fix folder variable pane layout 2025-10-24 14:53:07 -07:00
Gregory Schier
cf9882b5b9 Fix response viewer stream scrolling 2025-10-24 14:39:25 -07:00
Gregory Schier
bbf85c953d Better XML formatting, fix pointer cursor in sidebar, copy/create URL in response 2025-10-24 09:50:42 -07:00
Gregory Schier
17ddc76223 Better XML beautify 2025-10-24 08:59:16 -07:00
Gregory Schier
754ec0ba86 Fix AWS auth
https://x.com/NilsFleischer63/status/1981719735432511553
2025-10-24 08:42:18 -07:00
Gregory Schier
1198aa7d87 Add tree rename (on Enter) and global rename hotkeys (#279) 2025-10-24 08:01:38 -07:00
Gregory Schier
43437abae7 Add custom DNS resolver for *.localhost (#280) 2025-10-24 08:01:12 -07:00
moebiuscorzer
9439cfa2ba fix: typo 'validatation' corrected into 'validation' (#278) 2025-10-24 06:09:00 -07:00
gschier
a731ccc8bd Deploying to main from @ mountain-loop/yaak@451c8b9dde 🚀 2025-10-23 15:36:39 +00:00
Gregory Schier
451c8b9dde Fix PDF viewer 2025-10-22 08:56:36 -07:00
Gregory Schier
b7682db9a3 Remove duplicate themes in getThemes function 2025-10-22 06:56:00 -07:00
Gregory Schier
7e2d72c4e3 Fix secure() function editing 2025-10-21 20:09:56 -07:00
Gregory Schier
28bb460409 Add empty workspaces array to environment output fixture 2025-10-21 08:16:33 -07:00
Gregory Schier
56d635166b Add tsconfig.json for importer-postman-environment plugin 2025-10-21 08:08:27 -07:00
Gregory Schier
f6a7257104 Text color for selected 2025-10-21 07:46:55 -07:00
Gregory Schier
1fce060ef7 Npm i 2025-10-21 07:36:21 -07:00
Gregory Schier
5c966e5a95 Add bracket matching 2025-10-21 07:27:07 -07:00
Gregory Schier
0520ef5d43 Import postman environments
https://feedback.yaak.app/p/import-postman-environments
2025-10-21 07:20:37 -07:00
dependabot[bot]
25b110778a Bump vite from 7.0.7 to 7.0.8 (#269) 2025-10-20 21:19:55 -07:00
Gregory Schier
327bf84e57 Clarify proto import buttons 2025-10-20 09:23:12 -07:00
Gregory Schier
1c48b309b5 Fix indent guide hovering 2025-10-20 09:13:00 -07:00
Gregory Schier
7c5dec821d Remove React.lazy on overlay and tooltip 2025-10-19 12:00:30 -07:00
gschier
dcd8f6c08a Deploying to main from @ mountain-loop/yaak@31f9a63c3b 🚀 2025-10-19 17:19:55 +00:00
Gregory Schier
31f9a63c3b Don't force push 2025-10-19 10:18:32 -07:00
Gregory Schier
e902b67a63 Replace arrayMove with custom implementation in PairEditor to remove dependency on @dnd-kit/sortable. 2025-10-19 09:40:11 -07:00
Gregory Schier
b11c72fde4 Add back creation items to context menu 2025-10-19 08:52:03 -07:00
Gregory Schier
07b90c6ae3 Make plugins scrollable 2025-10-19 08:21:36 -07:00
Gregory Schier
ba6163b6d8 Better code splitting and removed final instances of react-dnd 2025-10-19 08:16:56 -07:00
Gregory Schier
8055b625d0 Improve handling of drag-and-drop for collapsed and empty folders in tree component 2025-10-18 07:59:14 -07:00
Gregory Schier
3a61ffbbb0 Better drag for empty folders 2025-10-18 07:41:33 -07:00
Gregory Schier
f8478677c5 Pass the previous app version to the notification endpoint so the update notification can display all missed changelogs, not just the latest one. 2025-10-18 07:13:52 -07:00
Gregory Schier
f5094c5a94 Fix drop marker 2025-10-17 16:15:14 -07:00
Étienne Lévesque
8300187566 [Plugins] [Auth] [oauth2] Support identity platforms with underlying IDPs (#261)
Co-authored-by: Gregory Schier <gschier1990@gmail.com>
2025-10-17 16:07:25 -07:00
Kien Dang
cd8ab3616e Fix GraphQL doc explorer CountBadge stacking order (#262) 2025-10-17 15:33:40 -07:00
Maksim Karelov
be0c92b755 Add ability to select fs.readFile encoding (#267) 2025-10-17 15:32:04 -07:00
Gregory Schier
c34ea20406 Flattened the sidebar tree 2025-10-17 15:07:02 -07:00
Gregory Schier
6e9b1db196 Bump version 2025-10-16 14:42:02 -07:00
Gregory Schier
d83aabd2be Dynamic template function args and TTL option for request chaining (#266) 2025-10-16 14:39:30 -07:00
Gregory Schier
d46479cd22 Remove debug console log from TreeDragOverlay component 2025-10-15 14:08:21 -07:00
Gregory Schier
19cae33382 Fix crash when delete after drag 2025-10-15 14:07:55 -07:00
Gregory Schier
267cd079ad New sidebar and folder view (#263) 2025-10-15 13:46:57 -07:00
164 changed files with 5636 additions and 2548 deletions

View File

@@ -40,4 +40,5 @@ jobs:
uses: JamesIves/github-pages-deploy-action@v4
with:
branch: main
force: false
folder: '.'

View File

@@ -22,7 +22,7 @@
<!-- sponsors-premium --><a href="https://github.com/MVST-Solutions"><img src="https:&#x2F;&#x2F;github.com&#x2F;MVST-Solutions.png" width="80px" alt="User avatar: MVST-Solutions" /></a>&nbsp;&nbsp;<a href="https://github.com/dharsanb"><img src="https:&#x2F;&#x2F;github.com&#x2F;dharsanb.png" width="80px" alt="User avatar: dharsanb" /></a>&nbsp;&nbsp;<a href="https://github.com/railwayapp"><img src="https:&#x2F;&#x2F;github.com&#x2F;railwayapp.png" width="80px" alt="User avatar: railwayapp" /></a>&nbsp;&nbsp;<a href="https://github.com/caseyamcl"><img src="https:&#x2F;&#x2F;github.com&#x2F;caseyamcl.png" width="80px" alt="User avatar: caseyamcl" /></a>&nbsp;&nbsp;<a href="https://github.com/andriyor"><img src="https:&#x2F;&#x2F;github.com&#x2F;andriyor.png" width="80px" alt="User avatar: andriyor" /></a>&nbsp;&nbsp;<a href="https://github.com/"><img src="https:&#x2F;&#x2F;raw.githubusercontent.com&#x2F;JamesIves&#x2F;github-sponsors-readme-action&#x2F;dev&#x2F;.github&#x2F;assets&#x2F;placeholder.png" width="80px" alt="User avatar: " /></a>&nbsp;&nbsp;<!-- sponsors-premium -->
</p>
<p align="center">
<!-- sponsors-base --><a href="https://github.com/seanwash"><img src="https:&#x2F;&#x2F;github.com&#x2F;seanwash.png" width="50px" alt="User avatar: seanwash" /></a>&nbsp;&nbsp;<a href="https://github.com/jerath"><img src="https:&#x2F;&#x2F;github.com&#x2F;jerath.png" width="50px" alt="User avatar: jerath" /></a>&nbsp;&nbsp;<a href="https://github.com/itsa-sh"><img src="https:&#x2F;&#x2F;github.com&#x2F;itsa-sh.png" width="50px" alt="User avatar: itsa-sh" /></a>&nbsp;&nbsp;<a href="https://github.com/dmmulroy"><img src="https:&#x2F;&#x2F;github.com&#x2F;dmmulroy.png" width="50px" alt="User avatar: dmmulroy" /></a>&nbsp;&nbsp;<a href="https://github.com/timcole"><img src="https:&#x2F;&#x2F;github.com&#x2F;timcole.png" width="50px" alt="User avatar: timcole" /></a>&nbsp;&nbsp;<a href="https://github.com/VLZH"><img src="https:&#x2F;&#x2F;github.com&#x2F;VLZH.png" width="50px" alt="User avatar: VLZH" /></a>&nbsp;&nbsp;<a href="https://github.com/terasaka2k"><img src="https:&#x2F;&#x2F;github.com&#x2F;terasaka2k.png" width="50px" alt="User avatar: terasaka2k" /></a>&nbsp;&nbsp;<!-- sponsors-base -->
<!-- sponsors-base --><a href="https://github.com/seanwash"><img src="https:&#x2F;&#x2F;github.com&#x2F;seanwash.png" width="50px" alt="User avatar: seanwash" /></a>&nbsp;&nbsp;<a href="https://github.com/jerath"><img src="https:&#x2F;&#x2F;github.com&#x2F;jerath.png" width="50px" alt="User avatar: jerath" /></a>&nbsp;&nbsp;<a href="https://github.com/itsa-sh"><img src="https:&#x2F;&#x2F;github.com&#x2F;itsa-sh.png" width="50px" alt="User avatar: itsa-sh" /></a>&nbsp;&nbsp;<a href="https://github.com/dmmulroy"><img src="https:&#x2F;&#x2F;github.com&#x2F;dmmulroy.png" width="50px" alt="User avatar: dmmulroy" /></a>&nbsp;&nbsp;<a href="https://github.com/timcole"><img src="https:&#x2F;&#x2F;github.com&#x2F;timcole.png" width="50px" alt="User avatar: timcole" /></a>&nbsp;&nbsp;<a href="https://github.com/VLZH"><img src="https:&#x2F;&#x2F;github.com&#x2F;VLZH.png" width="50px" alt="User avatar: VLZH" /></a>&nbsp;&nbsp;<a href="https://github.com/terasaka2k"><img src="https:&#x2F;&#x2F;github.com&#x2F;terasaka2k.png" width="50px" alt="User avatar: terasaka2k" /></a>&nbsp;&nbsp;<a href="https://github.com/majudhu"><img src="https:&#x2F;&#x2F;github.com&#x2F;majudhu.png" width="50px" alt="User avatar: majudhu" /></a>&nbsp;&nbsp;<a href="https://github.com/axelrindle"><img src="https:&#x2F;&#x2F;github.com&#x2F;axelrindle.png" width="50px" alt="User avatar: axelrindle" /></a>&nbsp;&nbsp;<!-- sponsors-base -->
</p>
![Yaak API Client](https://yaak.app/static/screenshot.png)

335
package-lock.json generated
View File

@@ -25,6 +25,7 @@
"plugins/importer-insomnia",
"plugins/importer-openapi",
"plugins/importer-postman",
"plugins/importer-postman-environment",
"plugins/importer-yaak",
"plugins/template-function-cookie",
"plugins/template-function-encode",
@@ -60,7 +61,7 @@
"@eslint/compat": "^1.3.0",
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.29.0",
"@tauri-apps/cli": "^2.8.4",
"@tauri-apps/cli": "^2.9.1",
"@typescript-eslint/eslint-plugin": "^8.27.0",
"@typescript-eslint/parser": "^8.27.0",
"@yaakapp/cli": "^0.2.7",
@@ -820,6 +821,45 @@
"integrity": "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==",
"license": "MIT"
},
"node_modules/@dnd-kit/accessibility": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
"integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==",
"license": "MIT",
"dependencies": {
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@dnd-kit/core": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
"license": "MIT",
"dependencies": {
"@dnd-kit/accessibility": "^3.1.1",
"@dnd-kit/utilities": "^3.2.2",
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/@dnd-kit/utilities": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz",
"integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==",
"license": "MIT",
"dependencies": {
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.6",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.6.tgz",
@@ -1930,24 +1970,6 @@
"node": ">=14"
}
},
"node_modules/@react-dnd/asap": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-5.0.2.tgz",
"integrity": "sha512-WLyfoHvxhs0V9U+GTsGilGgf2QsPl6ZZ44fnv0/b8T3nQyvzxidxsg/ZltbWssbsRDlYW8UKSQMTGotuTotZ6A==",
"license": "MIT"
},
"node_modules/@react-dnd/invariant": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-4.0.2.tgz",
"integrity": "sha512-xKCTqAK/FFauOM9Ta2pswIyT3D8AQlfrYdOi/toTPEhqCuAs1v5tcJ3Y08Izh1cJ5Jchwy9SeAXmMg6zrKs2iw==",
"license": "MIT"
},
"node_modules/@react-dnd/shallowequal": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-4.0.2.tgz",
"integrity": "sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==",
"license": "MIT"
},
"node_modules/@replit/codemirror-emacs": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/@replit/codemirror-emacs/-/codemirror-emacs-6.1.0.tgz",
@@ -2815,6 +2837,7 @@
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/container-queries/-/container-queries-0.1.1.tgz",
"integrity": "sha512-p18dswChx6WnTSaJCSGx6lTmrGzNNvm2FtXmiO6AuA1V4U5REyoqwmT6kgAsIMdjo07QdAfYXHJ4hnMtfHzWgA==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"tailwindcss": ">=3.2.0"
@@ -2834,9 +2857,9 @@
}
},
"node_modules/@tanstack/history": {
"version": "1.121.34",
"resolved": "https://registry.npmjs.org/@tanstack/history/-/history-1.121.34.tgz",
"integrity": "sha512-YL8dGi5ZU+xvtav2boRlw4zrRghkY6hvdcmHhA0RGSJ/CBgzv+cbADW9eYJLx74XMZvIQ1pp6VMbrpXnnM5gHA==",
"version": "1.133.3",
"resolved": "https://registry.npmjs.org/@tanstack/history/-/history-1.133.3.tgz",
"integrity": "sha512-zFQnGdX0S4g5xRuS+95iiEXM+qlGvYG7ksmOKx7LaMv60lDWa0imR8/24WwXXvBWJT1KnwVdZcjvhCwz9IiJCw==",
"license": "MIT",
"engines": {
"node": ">=12"
@@ -2847,9 +2870,9 @@
}
},
"node_modules/@tanstack/query-core": {
"version": "5.83.0",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.83.0.tgz",
"integrity": "sha512-0M8dA+amXUkyz5cVUm/B+zSk3xkQAcuXuz5/Q/LveT4ots2rBpPTZOzd7yJa2Utsf8D2Upl5KyjhHRY+9lB/XA==",
"version": "5.90.5",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.5.tgz",
"integrity": "sha512-wLamYp7FaDq6ZnNehypKI5fNvxHPfTYylE0m/ZpuuzJfJqhR5Pxg9gvGBHZx4n7J+V5Rg5mZxHHTlv25Zt5u+w==",
"license": "MIT",
"funding": {
"type": "github",
@@ -2857,12 +2880,12 @@
}
},
"node_modules/@tanstack/react-query": {
"version": "5.83.0",
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.83.0.tgz",
"integrity": "sha512-/XGYhZ3foc5H0VM2jLSD/NyBRIOK4q9kfeml4+0x2DlL6xVuAcVEW+hTlTapAmejObg0i3eNqhkr2dT+eciwoQ==",
"version": "5.90.5",
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.5.tgz",
"integrity": "sha512-pN+8UWpxZkEJ/Rnnj2v2Sxpx1WFlaa9L6a4UO89p6tTQbeo+m0MS8oYDjbggrR8QcTyjKoYWKS3xJQGr3ExT8Q==",
"license": "MIT",
"dependencies": {
"@tanstack/query-core": "5.83.0"
"@tanstack/query-core": "5.90.5"
},
"funding": {
"type": "github",
@@ -2873,14 +2896,14 @@
}
},
"node_modules/@tanstack/react-router": {
"version": "1.127.3",
"resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.127.3.tgz",
"integrity": "sha512-QprmWHJrGbEKXJiP7WZ+dilTJRc7nMbsFCUnfAUw8PsOYanhgvBkBwAU05YEo8WTIZ9atCc1R90hyzqbiBFkdA==",
"version": "1.133.13",
"resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.133.13.tgz",
"integrity": "sha512-mVAj70mPOH/a60Hjlha3gHEWLFuE4kHeKau/AL5Xp6e5GtNk1JTRwN4sJ9QlSyLcClOUUtGfED1FoLj0D2W0Eg==",
"license": "MIT",
"dependencies": {
"@tanstack/history": "1.121.34",
"@tanstack/history": "1.133.3",
"@tanstack/react-store": "^0.7.0",
"@tanstack/router-core": "1.127.3",
"@tanstack/router-core": "1.133.13",
"isbot": "^5.1.22",
"tiny-invariant": "^1.3.3",
"tiny-warning": "^1.0.3"
@@ -2933,14 +2956,14 @@
}
},
"node_modules/@tanstack/router-core": {
"version": "1.127.3",
"resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.127.3.tgz",
"integrity": "sha512-08JlfwsMIDkMyCQsRviMVBn0cVUzlNzkll4pZgf6QRSO1RASBsci5hMojcsdH0d/yXLH0FBJ6fINbj0ctBm63Q==",
"version": "1.133.13",
"resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.133.13.tgz",
"integrity": "sha512-zZptdlS/wSkqozb07Y3zX5gas2OapJdjEG6/Id0e/twNefVdR4EY2TK/mgvyhHtKIpCxIcnZz/3opypgeQi9bg==",
"license": "MIT",
"dependencies": {
"@tanstack/history": "1.121.34",
"@tanstack/history": "1.133.3",
"@tanstack/store": "^0.7.0",
"cookie-es": "^1.2.2",
"cookie-es": "^2.0.0",
"seroval": "^1.3.2",
"seroval-plugins": "^1.3.2",
"tiny-invariant": "^1.3.3",
@@ -3089,9 +3112,9 @@
}
},
"node_modules/@tauri-apps/api": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.8.0.tgz",
"integrity": "sha512-ga7zdhbS2GXOMTIZRT0mYjKJtR9fivsXzsyq5U3vjDL0s6DTMwYRm0UHNjzTY5dh4+LSC68Sm/7WEiimbQNYlw==",
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.9.0.tgz",
"integrity": "sha512-qD5tMjh7utwBk9/5PrTA/aGr3i5QaJ/Mlt7p8NilQ45WgbifUNPyKWsA63iQ8YfQq6R8ajMapU+/Q8nMcPRLNw==",
"license": "Apache-2.0 OR MIT",
"funding": {
"type": "opencollective",
@@ -3099,9 +3122,9 @@
}
},
"node_modules/@tauri-apps/cli": {
"version": "2.8.4",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.8.4.tgz",
"integrity": "sha512-ejUZBzuQRcjFV+v/gdj/DcbyX/6T4unZQjMSBZwLzP/CymEjKcc2+Fc8xTORThebHDUvqoXMdsCZt8r+hyN15g==",
"version": "2.9.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.9.1.tgz",
"integrity": "sha512-kKi2/WWsNXKoMdatBl4xrT7e1Ce27JvsetBVfWuIb6D3ep/Y0WO5SIr70yarXOSWam8NyDur4ipzjZkg6m7VDg==",
"dev": true,
"license": "Apache-2.0 OR MIT",
"bin": {
@@ -3115,23 +3138,23 @@
"url": "https://opencollective.com/tauri"
},
"optionalDependencies": {
"@tauri-apps/cli-darwin-arm64": "2.8.4",
"@tauri-apps/cli-darwin-x64": "2.8.4",
"@tauri-apps/cli-linux-arm-gnueabihf": "2.8.4",
"@tauri-apps/cli-linux-arm64-gnu": "2.8.4",
"@tauri-apps/cli-linux-arm64-musl": "2.8.4",
"@tauri-apps/cli-linux-riscv64-gnu": "2.8.4",
"@tauri-apps/cli-linux-x64-gnu": "2.8.4",
"@tauri-apps/cli-linux-x64-musl": "2.8.4",
"@tauri-apps/cli-win32-arm64-msvc": "2.8.4",
"@tauri-apps/cli-win32-ia32-msvc": "2.8.4",
"@tauri-apps/cli-win32-x64-msvc": "2.8.4"
"@tauri-apps/cli-darwin-arm64": "2.9.1",
"@tauri-apps/cli-darwin-x64": "2.9.1",
"@tauri-apps/cli-linux-arm-gnueabihf": "2.9.1",
"@tauri-apps/cli-linux-arm64-gnu": "2.9.1",
"@tauri-apps/cli-linux-arm64-musl": "2.9.1",
"@tauri-apps/cli-linux-riscv64-gnu": "2.9.1",
"@tauri-apps/cli-linux-x64-gnu": "2.9.1",
"@tauri-apps/cli-linux-x64-musl": "2.9.1",
"@tauri-apps/cli-win32-arm64-msvc": "2.9.1",
"@tauri-apps/cli-win32-ia32-msvc": "2.9.1",
"@tauri-apps/cli-win32-x64-msvc": "2.9.1"
}
},
"node_modules/@tauri-apps/cli-darwin-arm64": {
"version": "2.8.4",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.8.4.tgz",
"integrity": "sha512-BKu8HRkYV01SMTa7r4fLx+wjgtRK8Vep7lmBdHDioP6b8XH3q2KgsAyPWfEZaZIkZ2LY4SqqGARaE9oilNe0oA==",
"version": "2.9.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.9.1.tgz",
"integrity": "sha512-sdwhtsE/6njD0AjgfYEj1JyxZH4SBmCJSXpRm6Ph5fQeuZD6MyjzjdVOrrtFguyREVQ7xn0Ujkwvbo01ULthNg==",
"cpu": [
"arm64"
],
@@ -3146,9 +3169,9 @@
}
},
"node_modules/@tauri-apps/cli-darwin-x64": {
"version": "2.8.4",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.8.4.tgz",
"integrity": "sha512-imb9PfSd/7G6VAO7v1bQ2A3ZH4NOCbhGJFLchxzepGcXf9NKkfun157JH9mko29K6sqAwuJ88qtzbKCbWJTH9g==",
"version": "2.9.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.9.1.tgz",
"integrity": "sha512-c86g+67wTdI4TUCD7CaSd/13+oYuLQxVST4ZNJ5C+6i1kdnU3Us1L68N9MvbDLDQGJc9eo0pvuK6sCWkee+BzA==",
"cpu": [
"x64"
],
@@ -3163,9 +3186,9 @@
}
},
"node_modules/@tauri-apps/cli-linux-arm-gnueabihf": {
"version": "2.8.4",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.8.4.tgz",
"integrity": "sha512-Ml215UnDdl7/fpOrF1CNovym/KjtUbCuPgrcZ4IhqUCnhZdXuphud/JT3E8X97Y03TZ40Sjz8raXYI2ET0exzw==",
"version": "2.9.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.9.1.tgz",
"integrity": "sha512-IrB3gFQmueQKJjjisOcMktW/Gh6gxgqYO419doA3YZ7yIV5rbE8ZW52Q3I4AO+SlFEyVYer5kpi066p0JBlLGw==",
"cpu": [
"arm"
],
@@ -3180,9 +3203,9 @@
}
},
"node_modules/@tauri-apps/cli-linux-arm64-gnu": {
"version": "2.8.4",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.8.4.tgz",
"integrity": "sha512-pbcgBpMyI90C83CxE5REZ9ODyIlmmAPkkJXtV398X3SgZEIYy5TACYqlyyv2z5yKgD8F8WH4/2fek7+jH+ZXAw==",
"version": "2.9.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.9.1.tgz",
"integrity": "sha512-Ke7TyXvu6HbWSkmVkFbbH19D3cLsd117YtXP/u9NIvSpYwKeFtnbpirrIUfPm44Q+PZFZ2Hvg8X9qoUiAK0zKw==",
"cpu": [
"arm64"
],
@@ -3197,9 +3220,9 @@
}
},
"node_modules/@tauri-apps/cli-linux-arm64-musl": {
"version": "2.8.4",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.8.4.tgz",
"integrity": "sha512-zumFeaU1Ws5Ay872FTyIm7z8kfzEHu8NcIn8M6TxbJs0a7GRV21KBdpW1zNj2qy7HynnpQCqjAYXTUUmm9JAOw==",
"version": "2.9.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.9.1.tgz",
"integrity": "sha512-sGvy75sv55oeMulR5ArwPD28DsDQxqTzLhXCrpU9/nbFg/JImmI7k994YE9fr3V0qE3Cjk5gjLldRNv7I9sjwQ==",
"cpu": [
"arm64"
],
@@ -3214,9 +3237,9 @@
}
},
"node_modules/@tauri-apps/cli-linux-riscv64-gnu": {
"version": "2.8.4",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.8.4.tgz",
"integrity": "sha512-qiqbB3Zz6IyO201f+1ojxLj65WYj8mixL5cOMo63nlg8CIzsP23cPYUrx1YaDPsCLszKZo7tVs14pc7BWf+/aQ==",
"version": "2.9.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.9.1.tgz",
"integrity": "sha512-tEKbJydV3BdIxpAx8aGHW6VDg1xW4LlQuRD/QeFZdZNTreHJpMbJEcdvAcI+Hg6vgQpVpaoEldR9W4F6dYSLqQ==",
"cpu": [
"riscv64"
],
@@ -3231,9 +3254,9 @@
}
},
"node_modules/@tauri-apps/cli-linux-x64-gnu": {
"version": "2.8.4",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.8.4.tgz",
"integrity": "sha512-TaqaDd9Oy6k45Hotx3pOf+pkbsxLaApv4rGd9mLuRM1k6YS/aw81YrsMryYPThrxrScEIUcmNIHaHsLiU4GMkw==",
"version": "2.9.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.9.1.tgz",
"integrity": "sha512-mg5msXHagtHpyCVWgI01M26JeSrgE/otWyGdYcuTwyRYZYEJRTbcNt7hscOkdNlPBe7isScW7PVKbxmAjJJl4g==",
"cpu": [
"x64"
],
@@ -3248,9 +3271,9 @@
}
},
"node_modules/@tauri-apps/cli-linux-x64-musl": {
"version": "2.8.4",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.8.4.tgz",
"integrity": "sha512-ot9STAwyezN8w+bBHZ+bqSQIJ0qPZFlz/AyscpGqB/JnJQVDFQcRDmUPFEaAtt2UUHSWzN3GoTJ5ypqLBp2WQA==",
"version": "2.9.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.9.1.tgz",
"integrity": "sha512-lFZEXkpDreUe3zKilvnMsrnKP9gwQudaEjDnOz/GMzbzNceIuPfFZz0cR/ky1Aoq4eSvZonPKHhROq4owz4fzg==",
"cpu": [
"x64"
],
@@ -3265,9 +3288,9 @@
}
},
"node_modules/@tauri-apps/cli-win32-arm64-msvc": {
"version": "2.8.4",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.8.4.tgz",
"integrity": "sha512-+2aJ/g90dhLiOLFSD1PbElXX3SoMdpO7HFPAZB+xot3CWlAZD1tReUFy7xe0L5GAR16ZmrxpIDM9v9gn5xRy/w==",
"version": "2.9.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.9.1.tgz",
"integrity": "sha512-ejc5RAp/Lm1Aj0EQHaT+Wdt5PHfdgQV5hIDV00MV6HNbIb5W4ZUFxMDaRkAg65gl9MvY2fH396riePW3RoKXDw==",
"cpu": [
"arm64"
],
@@ -3282,9 +3305,9 @@
}
},
"node_modules/@tauri-apps/cli-win32-ia32-msvc": {
"version": "2.8.4",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.8.4.tgz",
"integrity": "sha512-yj7WDxkL1t9Uzr2gufQ1Hl7hrHuFKTNEOyascbc109EoiAqCp0tgZ2IykQqOZmZOHU884UAWI1pVMqBhS/BfhA==",
"version": "2.9.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.9.1.tgz",
"integrity": "sha512-fSATtJDc0fNjVB6ystyi8NbwhNFk8i8E05h6KrsC8Fio5eaJIJvPCbC9pdrPl6kkxN1X7fj25ErBbgfqgcK8Fg==",
"cpu": [
"ia32"
],
@@ -3299,9 +3322,9 @@
}
},
"node_modules/@tauri-apps/cli-win32-x64-msvc": {
"version": "2.8.4",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.8.4.tgz",
"integrity": "sha512-XuvGB4ehBdd7QhMZ9qbj/8icGEatDuBNxyYHbLKsTYh90ggUlPa/AtaqcC1Fo69lGkTmq9BOKrs1aWSi7xDonA==",
"version": "2.9.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.9.1.tgz",
"integrity": "sha512-/JHlOzpUDhjBOO9w167bcYxfJbcMQv7ykS/Y07xjtcga8np0rzUzVGWYmLMH7orKcDMC7wjhheEW1x8cbGma/Q==",
"cpu": [
"x64"
],
@@ -4167,6 +4190,10 @@
"resolved": "plugins/importer-postman",
"link": true
},
"node_modules/@yaak/importer-postman-environment": {
"resolved": "plugins/importer-postman-environment",
"link": true
},
"node_modules/@yaak/importer-yaak": {
"resolved": "plugins/importer-yaak",
"link": true
@@ -5876,9 +5903,9 @@
"license": "MIT"
},
"node_modules/cookie-es": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-1.2.2.tgz",
"integrity": "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-2.0.0.tgz",
"integrity": "sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg==",
"license": "MIT"
},
"node_modules/copy-descriptor": {
@@ -6668,17 +6695,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/dnd-core": {
"version": "16.0.1",
"resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-16.0.1.tgz",
"integrity": "sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng==",
"license": "MIT",
"dependencies": {
"@react-dnd/asap": "^5.0.1",
"@react-dnd/invariant": "^4.0.1",
"redux": "^4.2.0"
}
},
"node_modules/doctrine": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
@@ -9234,15 +9250,6 @@
"@babel/runtime": "^7.7.6"
}
},
"node_modules/hoist-non-react-statics": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
"integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
"license": "BSD-3-Clause",
"dependencies": {
"react-is": "^16.7.0"
}
},
"node_modules/hosted-git-info": {
"version": "2.8.9",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
@@ -14382,45 +14389,6 @@
"react-dom": ">=16.8.0"
}
},
"node_modules/react-dnd": {
"version": "16.0.1",
"resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-16.0.1.tgz",
"integrity": "sha512-QeoM/i73HHu2XF9aKksIUuamHPDvRglEwdHL4jsp784BgUuWcg6mzfxT0QDdQz8Wj0qyRKx2eMg8iZtWvU4E2Q==",
"license": "MIT",
"dependencies": {
"@react-dnd/invariant": "^4.0.1",
"@react-dnd/shallowequal": "^4.0.1",
"dnd-core": "^16.0.1",
"fast-deep-equal": "^3.1.3",
"hoist-non-react-statics": "^3.3.2"
},
"peerDependencies": {
"@types/hoist-non-react-statics": ">= 3.3.1",
"@types/node": ">= 12",
"@types/react": ">= 16",
"react": ">= 16.14"
},
"peerDependenciesMeta": {
"@types/hoist-non-react-statics": {
"optional": true
},
"@types/node": {
"optional": true
},
"@types/react": {
"optional": true
}
}
},
"node_modules/react-dnd-html5-backend": {
"version": "16.0.1",
"resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-16.0.1.tgz",
"integrity": "sha512-Wu3dw5aDJmOGw8WjH1I1/yTH+vlXEL4vmjk5p+MHxP8HuHJS1lAGeIdG/hze1AvNeXWo/JgULV87LyQOr+r5jw==",
"license": "MIT",
"dependencies": {
"dnd-core": "^16.0.1"
}
},
"node_modules/react-dom": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
@@ -14439,6 +14407,7 @@
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"dev": true,
"license": "MIT"
},
"node_modules/react-markdown": {
@@ -14807,15 +14776,6 @@
"node": ">=8"
}
},
"node_modules/redux": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",
"integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.9.2"
}
},
"node_modules/reflect.getprototypeof": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
@@ -16867,6 +16827,7 @@
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz",
"integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==",
"dev": true,
"license": "MIT",
"peer": true
},
@@ -17896,9 +17857,9 @@
}
},
"node_modules/vite": {
"version": "7.0.7",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.0.7.tgz",
"integrity": "sha512-hc6LujN/EkJHmxeiDJMs0qBontZ1cdBvvoCbWhVjzUFTU329VRyOC46gHNSA8NcOC5yzCeXpwI40tieI3DEZqg==",
"version": "7.0.8",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.0.8.tgz",
"integrity": "sha512-cJBdq0/u+8rgstg9t7UkBilf8ipLmeXJO30NxD5HAHOivnj10ocV8YtR/XBvd2wQpN3TmcaxNKaHX3tN7o5F5A==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -18564,26 +18525,11 @@
}
}
},
"node_modules/xml-formatter": {
"version": "3.6.6",
"resolved": "https://registry.npmjs.org/xml-formatter/-/xml-formatter-3.6.6.tgz",
"integrity": "sha512-yfofQht42x2sN1YThT6Er6GFXiQinfDAsMTNvMPi2uZw5/Vtc2PYHfvALR8U+b2oN2ekBxLd2tGWV06rAM8nQA==",
"license": "MIT",
"dependencies": {
"xml-parser-xo": "^4.1.4"
},
"engines": {
"node": ">= 16"
}
},
"node_modules/xml-parser-xo": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/xml-parser-xo/-/xml-parser-xo-4.1.4.tgz",
"integrity": "sha512-wo+yWDNeMwd1ctzH4CsiGXaAappDsxuR+VnmPewOzHk/zvefksT2ZlcWpAePl11THOWgnIZM4GjvumevurNWZw==",
"license": "MIT",
"engines": {
"node": ">= 16"
}
"node_modules/xml-beautify": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/xml-beautify/-/xml-beautify-1.2.3.tgz",
"integrity": "sha512-VsYpkqoVawIP84pi00XukPsgQHqOhgrpwTHlXqqRMAgYZ1u+Yw3KHIUhO1Igf19d5CQ5h6ExJT1hFCJRLmzADg==",
"license": "MIT"
},
"node_modules/xpath": {
"version": "0.0.34",
@@ -18824,7 +18770,7 @@
},
"packages/plugin-runtime-types": {
"name": "@yaakapp/api",
"version": "0.6.6",
"version": "0.7.0",
"dependencies": {
"@types/node": "^24.0.13"
},
@@ -18926,6 +18872,10 @@
"name": "@yaak/importer-postman",
"version": "0.1.0"
},
"plugins/importer-postman-environment": {
"name": "@yaak/importer-postman-environment",
"version": "0.1.0"
},
"plugins/importer-yaak": {
"name": "@yaak/importer-yaak",
"version": "0.1.0"
@@ -19089,17 +19039,17 @@
"@codemirror/lang-xml": "^6.1.0",
"@codemirror/language": "^6.11.0",
"@codemirror/search": "^6.5.11",
"@dnd-kit/core": "^6.3.1",
"@gilbarbara/deep-equal": "^0.3.1",
"@lezer/highlight": "^1.1.3",
"@lezer/lr": "^1.3.3",
"@replit/codemirror-emacs": "^6.1.0",
"@replit/codemirror-vim": "^6.3.0",
"@replit/codemirror-vscode-keymap": "^6.0.2",
"@tailwindcss/container-queries": "^0.1.1",
"@tanstack/react-query": "^5.76.1",
"@tanstack/react-router": "^1.120.3",
"@tanstack/react-virtual": "^3.13.8",
"@tauri-apps/api": "^2.8.0",
"@tanstack/react-query": "^5.90.5",
"@tanstack/react-router": "^1.133.13",
"@tanstack/react-virtual": "^3.13.12",
"@tauri-apps/api": "^2.9.0",
"@tauri-apps/plugin-clipboard-manager": "^2.3.0",
"@tauri-apps/plugin-dialog": "^2.4.0",
"@tauri-apps/plugin-fs": "^2.4.2",
@@ -19128,8 +19078,6 @@
"parse-color": "^1.0.0",
"react": "^19.1.0",
"react-colorful": "^5.6.1",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dom": "^19.1.0",
"react-markdown": "^10.1.0",
"react-pdf": "^10.0.1",
@@ -19141,11 +19089,12 @@
"slugify": "^1.6.6",
"uuid": "^11.1.0",
"whatwg-mimetype": "^4.0.0",
"xml-formatter": "^3.6.3",
"xml-beautify": "^1.2.3",
"yaml": "^2.6.1"
},
"devDependencies": {
"@lezer/generator": "^1.8.0",
"@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/nesting": "^0.0.0-insiders.565cd3e",
"@tanstack/router-plugin": "^1.127.5",
"@types/node": "^24.0.13",
@@ -19164,7 +19113,7 @@
"postcss": "^8.5.6",
"postcss-nesting": "^13.0.2",
"tailwindcss": "^3.4.17",
"vite": "^7.0.7",
"vite": "^7.0.8",
"vite-plugin-static-copy": "^3.1.2",
"vite-plugin-svgr": "^4.3.0",
"vite-plugin-top-level-await": "^1.5.0",

View File

@@ -24,6 +24,7 @@
"plugins/importer-insomnia",
"plugins/importer-openapi",
"plugins/importer-postman",
"plugins/importer-postman-environment",
"plugins/importer-yaak",
"plugins/template-function-cookie",
"plugins/template-function-encode",
@@ -80,7 +81,7 @@
"@eslint/compat": "^1.3.0",
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.29.0",
"@tauri-apps/cli": "^2.8.4",
"@tauri-apps/cli": "^2.9.1",
"@typescript-eslint/eslint-plugin": "^8.27.0",
"@typescript-eslint/parser": "^8.27.0",
"@yaakapp/cli": "^0.2.7",

View File

@@ -1,6 +1,6 @@
{
"name": "@yaakapp/api",
"version": "0.6.6",
"version": "0.7.0",
"keywords": [
"api-client",
"insomnia-alternative",

View File

@@ -361,7 +361,11 @@ export type GetKeyValueRequest = { key: string, };
export type GetKeyValueResponse = { value?: string, };
export type GetTemplateFunctionsResponse = { functions: Array<TemplateFunction>, pluginRefId: string, };
export type GetTemplateFunctionConfigRequest = { contextId: string, name: string, values: { [key in string]?: JsonPrimitive }, };
export type GetTemplateFunctionConfigResponse = { function: TemplateFunction, pluginRefId: string, };
export type GetTemplateFunctionSummaryResponse = { functions: Array<TemplateFunction>, pluginRefId: string, };
export type GetThemesRequest = Record<string, never>;
@@ -385,7 +389,7 @@ export type ImportResponse = { resources: ImportResources, };
export type InternalEvent = { id: string, pluginRefId: string, pluginName: string, replyId: string | null, windowContext: PluginWindowContext, payload: InternalEventPayload, };
export type InternalEventPayload = { "type": "boot_request" } & BootRequest | { "type": "boot_response" } | { "type": "reload_response" } & ReloadResponse | { "type": "terminate_request" } | { "type": "terminate_response" } | { "type": "import_request" } & ImportRequest | { "type": "import_response" } & ImportResponse | { "type": "filter_request" } & FilterRequest | { "type": "filter_response" } & FilterResponse | { "type": "export_http_request_request" } & ExportHttpRequestRequest | { "type": "export_http_request_response" } & ExportHttpRequestResponse | { "type": "send_http_request_request" } & SendHttpRequestRequest | { "type": "send_http_request_response" } & SendHttpRequestResponse | { "type": "list_cookie_names_request" } & ListCookieNamesRequest | { "type": "list_cookie_names_response" } & ListCookieNamesResponse | { "type": "get_cookie_value_request" } & GetCookieValueRequest | { "type": "get_cookie_value_response" } & GetCookieValueResponse | { "type": "get_http_request_actions_request" } & EmptyPayload | { "type": "get_http_request_actions_response" } & GetHttpRequestActionsResponse | { "type": "call_http_request_action_request" } & CallHttpRequestActionRequest | { "type": "get_grpc_request_actions_request" } & EmptyPayload | { "type": "get_grpc_request_actions_response" } & GetGrpcRequestActionsResponse | { "type": "call_grpc_request_action_request" } & CallGrpcRequestActionRequest | { "type": "get_template_functions_request" } | { "type": "get_template_functions_response" } & GetTemplateFunctionsResponse | { "type": "call_template_function_request" } & CallTemplateFunctionRequest | { "type": "call_template_function_response" } & CallTemplateFunctionResponse | { "type": "get_http_authentication_summary_request" } & EmptyPayload | { "type": "get_http_authentication_summary_response" } & GetHttpAuthenticationSummaryResponse | { "type": "get_http_authentication_config_request" } & GetHttpAuthenticationConfigRequest | { "type": "get_http_authentication_config_response" } & GetHttpAuthenticationConfigResponse | { "type": "call_http_authentication_request" } & CallHttpAuthenticationRequest | { "type": "call_http_authentication_response" } & CallHttpAuthenticationResponse | { "type": "call_http_authentication_action_request" } & CallHttpAuthenticationActionRequest | { "type": "call_http_authentication_action_response" } & EmptyPayload | { "type": "copy_text_request" } & CopyTextRequest | { "type": "copy_text_response" } & EmptyPayload | { "type": "render_http_request_request" } & RenderHttpRequestRequest | { "type": "render_http_request_response" } & RenderHttpRequestResponse | { "type": "render_grpc_request_request" } & RenderGrpcRequestRequest | { "type": "render_grpc_request_response" } & RenderGrpcRequestResponse | { "type": "get_key_value_request" } & GetKeyValueRequest | { "type": "get_key_value_response" } & GetKeyValueResponse | { "type": "set_key_value_request" } & SetKeyValueRequest | { "type": "set_key_value_response" } & SetKeyValueResponse | { "type": "delete_key_value_request" } & DeleteKeyValueRequest | { "type": "delete_key_value_response" } & DeleteKeyValueResponse | { "type": "open_window_request" } & OpenWindowRequest | { "type": "window_navigate_event" } & WindowNavigateEvent | { "type": "window_close_event" } | { "type": "close_window_request" } & CloseWindowRequest | { "type": "template_render_request" } & TemplateRenderRequest | { "type": "template_render_response" } & TemplateRenderResponse | { "type": "show_toast_request" } & ShowToastRequest | { "type": "show_toast_response" } & EmptyPayload | { "type": "prompt_text_request" } & PromptTextRequest | { "type": "prompt_text_response" } & PromptTextResponse | { "type": "get_http_request_by_id_request" } & GetHttpRequestByIdRequest | { "type": "get_http_request_by_id_response" } & GetHttpRequestByIdResponse | { "type": "find_http_responses_request" } & FindHttpResponsesRequest | { "type": "find_http_responses_response" } & FindHttpResponsesResponse | { "type": "get_themes_request" } & GetThemesRequest | { "type": "get_themes_response" } & GetThemesResponse | { "type": "empty_response" } & EmptyPayload | { "type": "error_response" } & ErrorResponse;
export type InternalEventPayload = { "type": "boot_request" } & BootRequest | { "type": "boot_response" } | { "type": "reload_response" } & ReloadResponse | { "type": "terminate_request" } | { "type": "terminate_response" } | { "type": "import_request" } & ImportRequest | { "type": "import_response" } & ImportResponse | { "type": "filter_request" } & FilterRequest | { "type": "filter_response" } & FilterResponse | { "type": "export_http_request_request" } & ExportHttpRequestRequest | { "type": "export_http_request_response" } & ExportHttpRequestResponse | { "type": "send_http_request_request" } & SendHttpRequestRequest | { "type": "send_http_request_response" } & SendHttpRequestResponse | { "type": "list_cookie_names_request" } & ListCookieNamesRequest | { "type": "list_cookie_names_response" } & ListCookieNamesResponse | { "type": "get_cookie_value_request" } & GetCookieValueRequest | { "type": "get_cookie_value_response" } & GetCookieValueResponse | { "type": "get_http_request_actions_request" } & EmptyPayload | { "type": "get_http_request_actions_response" } & GetHttpRequestActionsResponse | { "type": "call_http_request_action_request" } & CallHttpRequestActionRequest | { "type": "get_grpc_request_actions_request" } & EmptyPayload | { "type": "get_grpc_request_actions_response" } & GetGrpcRequestActionsResponse | { "type": "call_grpc_request_action_request" } & CallGrpcRequestActionRequest | { "type": "get_template_function_summary_request" } & EmptyPayload | { "type": "get_template_function_summary_response" } & GetTemplateFunctionSummaryResponse | { "type": "get_template_function_config_request" } & GetTemplateFunctionConfigRequest | { "type": "get_template_function_config_response" } & GetTemplateFunctionConfigResponse | { "type": "call_template_function_request" } & CallTemplateFunctionRequest | { "type": "call_template_function_response" } & CallTemplateFunctionResponse | { "type": "get_http_authentication_summary_request" } & EmptyPayload | { "type": "get_http_authentication_summary_response" } & GetHttpAuthenticationSummaryResponse | { "type": "get_http_authentication_config_request" } & GetHttpAuthenticationConfigRequest | { "type": "get_http_authentication_config_response" } & GetHttpAuthenticationConfigResponse | { "type": "call_http_authentication_request" } & CallHttpAuthenticationRequest | { "type": "call_http_authentication_response" } & CallHttpAuthenticationResponse | { "type": "call_http_authentication_action_request" } & CallHttpAuthenticationActionRequest | { "type": "call_http_authentication_action_response" } & EmptyPayload | { "type": "copy_text_request" } & CopyTextRequest | { "type": "copy_text_response" } & EmptyPayload | { "type": "render_http_request_request" } & RenderHttpRequestRequest | { "type": "render_http_request_response" } & RenderHttpRequestResponse | { "type": "render_grpc_request_request" } & RenderGrpcRequestRequest | { "type": "render_grpc_request_response" } & RenderGrpcRequestResponse | { "type": "template_render_request" } & TemplateRenderRequest | { "type": "template_render_response" } & TemplateRenderResponse | { "type": "get_key_value_request" } & GetKeyValueRequest | { "type": "get_key_value_response" } & GetKeyValueResponse | { "type": "set_key_value_request" } & SetKeyValueRequest | { "type": "set_key_value_response" } & SetKeyValueResponse | { "type": "delete_key_value_request" } & DeleteKeyValueRequest | { "type": "delete_key_value_response" } & DeleteKeyValueResponse | { "type": "open_window_request" } & OpenWindowRequest | { "type": "window_navigate_event" } & WindowNavigateEvent | { "type": "window_close_event" } | { "type": "close_window_request" } & CloseWindowRequest | { "type": "show_toast_request" } & ShowToastRequest | { "type": "show_toast_response" } & EmptyPayload | { "type": "prompt_text_request" } & PromptTextRequest | { "type": "prompt_text_response" } & PromptTextResponse | { "type": "get_http_request_by_id_request" } & GetHttpRequestByIdRequest | { "type": "get_http_request_by_id_response" } & GetHttpRequestByIdResponse | { "type": "find_http_responses_request" } & FindHttpResponsesRequest | { "type": "find_http_responses_response" } & FindHttpResponsesResponse | { "type": "get_themes_request" } & GetThemesRequest | { "type": "get_themes_response" } & GetThemesResponse | { "type": "empty_response" } & EmptyPayload | { "type": "error_response" } & ErrorResponse;
export type JsonPrimitive = string | number | boolean | null;

View File

@@ -1,12 +1,21 @@
import {
CallTemplateFunctionArgs,
FormInput,
GetHttpAuthenticationConfigRequest,
TemplateFunction,
} from "../bindings/gen_events";
import { Context } from "./Context";
TemplateFunctionArg,
} from '../bindings/gen_events';
import { MaybePromise } from '../helpers';
import { Context } from './Context';
export type DynamicTemplateFunctionArg = FormInput & {
dynamic(
ctx: Context,
args: GetHttpAuthenticationConfigRequest,
): MaybePromise<Partial<FormInput> | undefined | null>;
};
export type TemplateFunctionPlugin = TemplateFunction & {
onRender(
ctx: Context,
args: CallTemplateFunctionArgs,
): Promise<string | null>;
args: (TemplateFunctionArg | DynamicTemplateFunctionArg)[];
onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null>;
};

View File

@@ -186,20 +186,55 @@ export class PluginInstance {
}
if (
payload.type === 'get_template_functions_request' &&
payload.type === 'get_template_function_summary_request' &&
Array.isArray(this.#mod?.templateFunctions)
) {
const reply: TemplateFunction[] = this.#mod.templateFunctions.map((templateFunction) => {
return {
...migrateTemplateFunctionSelectOptions(templateFunction),
// Add everything except render
onRender: undefined,
};
});
const functions: TemplateFunction[] = this.#mod.templateFunctions.map(
(templateFunction) => {
return {
...migrateTemplateFunctionSelectOptions(templateFunction),
// Add everything except render
onRender: undefined,
};
},
);
const replyPayload: InternalEventPayload = {
type: 'get_template_functions_response',
type: 'get_template_function_summary_response',
pluginRefId: this.#workerData.pluginRefId,
functions: reply,
functions,
};
this.#sendPayload(windowContext, replyPayload, replyId);
return;
}
if (
payload.type === 'get_template_function_config_request' &&
Array.isArray(this.#mod?.templateFunctions)
) {
let templateFunction = this.#mod.templateFunctions.find((f) => f.name === payload.name);
if (templateFunction == null) {
this.#sendEmpty(windowContext, replyId);
return;
}
templateFunction = migrateTemplateFunctionSelectOptions(templateFunction);
// @ts-ignore
delete templateFunction.onRender;
const resolvedArgs: TemplateFunctionArg[] = [];
for (const arg of templateFunction.args) {
if (arg && 'dynamic' in arg) {
const dynamicAttrs = await arg.dynamic(ctx, payload);
const { dynamic, ...other } = arg;
resolvedArgs.push({ ...other, ...dynamicAttrs } as TemplateFunctionArg);
} else if (arg) {
resolvedArgs.push(arg);
}
templateFunction.args = resolvedArgs;
}
const replyPayload: InternalEventPayload = {
type: 'get_template_function_config_response',
pluginRefId: this.#workerData.pluginRefId,
function: templateFunction,
};
this.#sendPayload(windowContext, replyPayload, replyId);
return;

View File

@@ -1,6 +1,8 @@
import { TemplateFunction } from '@yaakapp/api';
import { TemplateFunctionPlugin } from '@yaakapp/api/lib/plugins/TemplateFunctionPlugin';
export function migrateTemplateFunctionSelectOptions(f: TemplateFunction): TemplateFunction {
export function migrateTemplateFunctionSelectOptions(
f: TemplateFunctionPlugin,
): TemplateFunctionPlugin {
const migratedArgs = f.args.map((a) => {
if (a.type === 'select') {
a.options = a.options.map((o) => ({

View File

@@ -57,16 +57,17 @@ export const plugin: PluginDefinition = {
}
}
// TODO: Support body signing here
headers['x-amz-content-sha256'] = 'UNSIGNED-PAYLOAD';
if (args.method !== 'GET') {
headers['x-amz-content-sha256'] = 'UNSIGNED-PAYLOAD';
}
const signature = aws4.sign(
{
host: url.host,
method: args.method,
path: url.pathname + (url.search || '') || undefined,
service: String(values.service || 'sts') || undefined,
region: String(values.region || 'us-east-1') || undefined,
path: url.pathname + (url.search || ''),
service: String(values.service || 'sts'),
region: values.region ? String(values.region) : undefined,
headers,
},
{
@@ -81,8 +82,6 @@ export const plugin: PluginDefinition = {
// - opts.headers["X-Amz-Date"]
// - optionally content sha256 header etc
console.log('ADDING STUFF', signature);
if (signature.headers == null) {
return {};
}

View File

@@ -12,6 +12,7 @@
"scripts": {
"build": "yaakcli build",
"dev": "yaakcli dev",
"lint":"tsc --noEmit && eslint . --ext .ts,.tsx"
"lint":"tsc --noEmit && eslint . --ext .ts,.tsx",
"test": "vitest --run tests"
}
}

View File

@@ -4,6 +4,7 @@ import { fetchAccessToken } from '../fetchAccessToken';
import { getOrRefreshAccessToken } from '../getOrRefreshAccessToken';
import type { AccessToken, TokenStoreArgs } from '../store';
import { getDataDirKey, storeToken } from '../store';
import { extractCode } from '../util';
export const PKCE_SHA256 = 'S256';
export const PKCE_PLAIN = 'plain';
@@ -79,7 +80,6 @@ export async function getAuthorizationCode(
authorizationUrl.searchParams.set('code_challenge_method', pkce.challengeMethod);
}
const logsEnabled = (await ctx.store.get('enable_logs')) ?? false;
const dataDirKey = await getDataDirKey(ctx, contextId);
const authorizationUrlStr = authorizationUrl.toString();
console.log('[oauth2] Authorizing', authorizationUrlStr);
@@ -97,18 +97,17 @@ export async function getAuthorizationCode(
}
},
async onNavigate({ url: urlStr }) {
const url = new URL(urlStr);
if (logsEnabled) console.log('[oauth2] Navigated to', urlStr);
if (url.searchParams.has('error')) {
let code;
try {
code = extractCode(urlStr, redirectUri);
} catch (err) {
reject(err);
close();
return reject(new Error(`Failed to authorize: ${url.searchParams.get('error')}`));
return;
}
const code = url.searchParams.get('code');
if (!code) {
console.log('[oauth2] Code not found');
return; // Could be one of many redirects in a chain, so skip it
return;
}
// Close the window here, because we don't need it anymore!

View File

@@ -6,8 +6,8 @@ import type {
PluginDefinition,
} from '@yaakapp/api';
import {
genPkceCodeVerifier,
DEFAULT_PKCE_METHOD,
genPkceCodeVerifier,
getAuthorizationCode,
PKCE_PLAIN,
PKCE_SHA256,
@@ -125,17 +125,6 @@ export const plugin: PluginDefinition = {
await resetDataDirKey(ctx, contextId);
},
},
{
label: 'Toggle Debug Logs',
async onSelect(ctx) {
const enableLogs = !(await ctx.store.get('enable_logs'));
await ctx.store.set('enable_logs', enableLogs);
await ctx.toast.show({
message: `Debug logs ${enableLogs ? 'enabled' : 'disabled'}`,
color: 'info',
});
},
},
],
args: [
{

View File

@@ -3,3 +3,83 @@ import type { AccessToken } from './store';
export function isTokenExpired(token: AccessToken) {
return token.expiresAt && Date.now() > token.expiresAt;
}
export function extractCode(urlStr: string, redirectUri: string | null): string | null {
const url = new URL(urlStr);
if (!urlMatchesRedirect(url, redirectUri)) {
console.log('[oauth2] URL does not match redirect origin/path; skipping.');
return null;
}
// Prefer query param; fall back to fragment if query lacks it
const query = url.searchParams;
const queryError = query.get('error');
const queryDesc = query.get('error_description');
const queryUri = query.get('error_uri');
let hashParams: URLSearchParams | null = null;
if (url.hash && url.hash.length > 1) {
hashParams = new URLSearchParams(url.hash.slice(1));
}
const hashError = hashParams?.get('error');
const hashDesc = hashParams?.get('error_description');
const hashUri = hashParams?.get('error_uri');
const error = queryError || hashError;
if (error) {
const desc = queryDesc || hashDesc;
const uri = queryUri || hashUri;
let message = `Failed to authorize: ${error}`;
if (desc) message += ` (${desc})`;
if (uri) message += ` [${uri}]`;
throw new Error(message);
}
const queryCode = query.get('code');
if (queryCode) return queryCode;
const hashCode = hashParams?.get('code');
if (hashCode) return hashCode;
console.log('[oauth2] Code not found');
return null;
}
export function urlMatchesRedirect(url: URL, redirectUrl: string | null): boolean {
if (!redirectUrl) return true;
let redirect;
try {
redirect = new URL(redirectUrl);
} catch {
console.log('[oauth2] Invalid redirect URI; skipping.');
return false;
}
const sameProtocol = url.protocol === redirect.protocol;
const sameHost = url.hostname.toLowerCase() === redirect.hostname.toLowerCase();
const normalizePort = (u: URL) =>
(u.protocol === 'https:' && (!u.port || u.port === '443')) ||
(u.protocol === 'http:' && (!u.port || u.port === '80'))
? ''
: u.port;
const samePort = normalizePort(url) === normalizePort(redirect);
const normPath = (p: string) => {
const withLeading = p.startsWith('/') ? p : `/${p}`;
// strip trailing slashes, keep root as "/"
return withLeading.replace(/\/+$/g, '') || '/';
};
// Require redirect path to be a prefix of the navigated URL path
const urlPath = normPath(url.pathname);
const redirectPath = normPath(redirect.pathname);
const pathMatches = urlPath === redirectPath || urlPath.startsWith(`${redirectPath}/`);
return sameProtocol && sameHost && samePort && pathMatches;
}

View File

@@ -0,0 +1,109 @@
import { describe, test, expect } from 'vitest';
import { extractCode } from '../src/util';
describe('extractCode', () => {
test('extracts code from query when same origin + path', () => {
const url = 'https://app.example.com/cb?code=abc123&state=xyz';
const redirect = 'https://app.example.com/cb';
expect(extractCode(url, redirect)).toBe('abc123');
});
test('extracts code from query with weird path', () => {
const url = 'https://app.example.com/cbwithextra?code=abc123&state=xyz';
const redirect = 'https://app.example.com/cb';
expect(extractCode(url, redirect)).toBeNull();
});
test('allows trailing slash differences', () => {
expect(extractCode('https://app.example.com/cb/?code=abc', 'https://app.example.com/cb')).toBe(
'abc',
);
expect(extractCode('https://app.example.com/cb?code=abc', 'https://app.example.com/cb/')).toBe(
'abc',
);
});
test('treats default ports as equal (https:443, http:80)', () => {
expect(
extractCode('https://app.example.com/cb?code=abc', 'https://app.example.com:443/cb'),
).toBe('abc');
expect(extractCode('http://app.example.com/cb?code=abc', 'http://app.example.com:80/cb')).toBe(
'abc',
);
});
test('rejects different port', () => {
expect(
extractCode('https://app.example.com/cb?code=abc', 'https://app.example.com:8443/cb'),
).toBeNull();
});
test('rejects different hostname (including subdomain changes)', () => {
expect(
extractCode('https://evil.example.com/cb?code=abc', 'https://app.example.com/cb'),
).toBeNull();
});
test('requires path to start with redirect path (ignoring query/hash)', () => {
// same origin but wrong path -> null
expect(
extractCode('https://app.example.com/other?code=abc', 'https://app.example.com/cb'),
).toBeNull();
// deeper subpath under the redirect path -> allowed (prefix match)
expect(
extractCode('https://app.example.com/cb/deep?code=abc', 'https://app.example.com/cb'),
).toBe('abc');
});
test('works with custom schemes', () => {
expect(extractCode('myapp://cb?code=abc', 'myapp://cb')).toBe('abc');
});
test('prefers query over fragment when both present', () => {
const url = 'https://app.example.com/cb?code=queryCode#code=hashCode';
const redirect = 'https://app.example.com/cb';
expect(extractCode(url, redirect)).toBe('queryCode');
});
test('extracts code from fragment when query lacks code', () => {
const url = 'https://app.example.com/cb#code=fromHash&state=xyz';
const redirect = 'https://app.example.com/cb';
expect(extractCode(url, redirect)).toBe('fromHash');
});
test('returns null if no code present (query or fragment)', () => {
const url = 'https://app.example.com/cb?state=only';
const redirect = 'https://app.example.com/cb';
expect(extractCode(url, redirect)).toBeNull();
});
test('returns null when provider reports an error', () => {
const url = 'https://app.example.com/cb?error=access_denied&error_description=oopsy';
const redirect = 'https://app.example.com/cb';
expect(() => extractCode(url, redirect)).toThrow('Failed to authorize: access_denied');
});
test('when redirectUri is null, extracts code from any URL', () => {
expect(extractCode('https://random.example.com/whatever?code=abc', null)).toBe('abc');
});
test('handles extra params gracefully', () => {
const url = 'https://app.example.com/cb?foo=1&bar=2&code=abc&baz=3';
const redirect = 'https://app.example.com/cb';
expect(extractCode(url, redirect)).toBe('abc');
});
test('ignores fragment noise when code is in query', () => {
const url = 'https://app.example.com/cb?code=abc#some=thing';
const redirect = 'https://app.example.com/cb';
expect(extractCode(url, redirect)).toBe('abc');
});
// If you decide NOT to support fragment-based codes, flip these to expect null or mark as .skip
test('supports fragment-only code for response_mode=fragment providers', () => {
const url = 'https://app.example.com/cb#state=xyz&code=abc';
const redirect = 'https://app.example.com/cb';
expect(extractCode(url, redirect)).toBe('abc');
});
});

View File

@@ -184,6 +184,7 @@ function importEnvironment(
workspaceId: string,
isParent?: boolean,
): PartialImportResources['environments'][0] {
isParent ??= e.parentId === workspaceId;
return {
id: convertId(e._id),
createdAt: e.created ? new Date(e.created).toISOString().replace('Z', '') : undefined,
@@ -192,7 +193,8 @@ function importEnvironment(
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
sortPriority: e.metaSortKey, // Will be added to Yaak later
base: isParent ?? e.parentId === workspaceId,
parentModel: isParent ? 'workspace' : 'environment',
parentId: null,
model: 'environment',
name: e.name,
variables: Object.entries(e.data).map(([name, value]) => ({

View File

@@ -5,7 +5,8 @@
"createdAt": "2025-01-13T15:15:43.767",
"updatedAt": "2025-01-13T15:15:55.209",
"sortPriority": 1736781343767,
"base": true,
"parentId": null,
"parentModel": "workspace",
"id": "GENERATE_ID::env_16c0dec5b77c414ae0e419b8f10c3701300c5900",
"model": "environment",
"name": "Base Environment",
@@ -22,7 +23,8 @@
"createdAt": "2025-01-13T15:15:58.515",
"updatedAt": "2025-01-13T15:16:34.705",
"sortPriority": 1736781358515,
"base": false,
"parentId": null,
"parentModel": "environment",
"id": "GENERATE_ID::env_799ae3d723ef44af91b4817e5d057e6d",
"model": "environment",
"name": "Production",
@@ -39,7 +41,8 @@
"createdAt": "2025-01-13T15:16:14.707",
"updatedAt": "2025-01-13T15:16:31.078",
"sortPriority": 1736781358565,
"base": false,
"parentId": null,
"parentModel": "environment",
"id": "GENERATE_ID::env_030fbfdbb274426ebd78e2e6518f8553",
"model": "environment",
"name": "Staging",

View File

@@ -0,0 +1,14 @@
{
"name": "@yaak/importer-postman-environment",
"displayName": "Postman Environment Importer",
"description": "Import environments from Postman",
"private": true,
"version": "0.1.0",
"main": "./build/index.js",
"scripts": {
"build": "yaakcli build",
"dev": "yaakcli dev",
"lint": "tsc --noEmit && eslint . --ext .ts,.tsx",
"test": "vitest --run tests"
}
}

View File

@@ -0,0 +1,138 @@
import type {
Context,
Environment,
PartialImportResources,
PluginDefinition,
Workspace,
} from '@yaakapp/api';
import type { ImportPluginResponse } from '@yaakapp/api/lib/plugins/ImporterPlugin';
type AtLeast<T, K extends keyof T> = Partial<T> & Pick<T, K>;
interface ExportResources {
workspaces: AtLeast<Workspace, 'name' | 'id' | 'model'>[];
environments: AtLeast<Environment, 'name' | 'id' | 'model' | 'workspaceId'>[];
}
export const plugin: PluginDefinition = {
importer: {
name: 'Postman Environment',
description: 'Import postman environment exports',
onImport(_ctx: Context, args: { text: string }) {
return convertPostmanEnvironment(args.text);
},
},
};
export function convertPostmanEnvironment(contents: string): ImportPluginResponse | undefined {
const root = parseJSONToRecord(contents);
if (root == null) return;
// Validate that it looks like a Postman Environment export
const values = toArray<{
key?: string;
value?: unknown;
enabled?: boolean;
description?: string;
type?: string;
}>(root.values);
const scope = root._postman_variable_scope;
const hasEnvMarkers = typeof scope === 'string';
if (values.length === 0 || (!hasEnvMarkers && typeof root.name !== 'string')) {
// Not a Postman environment file, skip
return;
}
const exportResources: ExportResources = {
workspaces: [],
environments: [],
};
const envVariables = values
.map((v) => ({
enabled: v.enabled ?? true,
name: String(v.key ?? ''),
value: String(v.value),
description: v.description ? String(v.description) : null,
}))
.filter((v) => v.name.length > 0);
const environment: ExportResources['environments'][0] = {
model: 'environment',
id: generateId('environment'),
name: root.name ? String(root.name) : 'Environment',
workspaceId: 'CURRENT_WORKSPACE',
parentModel: 'environment',
parentId: null,
variables: envVariables,
};
exportResources.environments.push(environment);
const resources = deleteUndefinedAttrs(
convertTemplateSyntax(exportResources),
) as PartialImportResources;
return { resources };
}
function parseJSONToRecord<T>(jsonStr: string): Record<string, T> | null {
try {
return toRecord(JSON.parse(jsonStr));
} catch {
return null;
}
}
function toRecord<T>(value: Record<string, T> | unknown): Record<string, T> {
if (value && typeof value === 'object' && !Array.isArray(value)) {
return value as Record<string, T>;
}
return {} as Record<string, T>;
}
function toArray<T>(value: unknown): T[] {
if (Object.prototype.toString.call(value) === '[object Array]') return value as T[];
else return [] as T[];
}
/** Recursively render all nested object properties */
function convertTemplateSyntax<T>(obj: T): T {
if (typeof obj === 'string') {
return obj.replace(
/{{\s*(_\.)?([^}]*)\s*}}/g,
(_m, _dot, expr) => '${[' + expr.trim() + ']}',
) as T;
} else if (Array.isArray(obj) && obj != null) {
return obj.map(convertTemplateSyntax) as T;
} else if (typeof obj === 'object' && obj != null) {
return Object.fromEntries(
Object.entries(obj as Record<string, unknown>).map(([k, v]) => [k, convertTemplateSyntax(v)]),
) as T;
} else {
return obj;
}
}
function deleteUndefinedAttrs<T>(obj: T): T {
if (Array.isArray(obj) && obj != null) {
return obj.map(deleteUndefinedAttrs) as T;
} else if (typeof obj === 'object' && obj != null) {
return Object.fromEntries(
Object.entries(obj as Record<string, unknown>)
.filter(([, v]) => v !== undefined)
.map(([k, v]) => [k, deleteUndefinedAttrs(v)]),
) as T;
} else {
return obj;
}
}
const idCount: Partial<Record<string, number>> = {};
function generateId(model: string): string {
idCount[model] = (idCount[model] ?? -1) + 1;
return `GENERATE_ID::${model.toUpperCase()}_${idCount[model]}`;
}
export default plugin;

View File

@@ -0,0 +1,27 @@
{
"id": "123",
"name": "My Environment",
"values": [
{
"key": "baseUrl",
"value": "https://api.example.com",
"type": "default",
"enabled": true
},
{
"key": "token",
"value": "{{ access_token }}",
"type": "default",
"description": "Access token for the API.",
"enabled": true
},
{
"key": "disabled",
"type": "secret",
"value": "hello",
"enabled": false
}
],
"_postman_variable_scope": "environment",
"_postman_exported_using": "PostmanRuntime/1.0.0"
}

View File

@@ -0,0 +1,35 @@
{
"resources": {
"workspaces": [],
"environments": [
{
"id": "GENERATE_ID::ENVIRONMENT_0",
"model": "environment",
"name": "My Environment",
"variables": [
{
"enabled": true,
"description": null,
"name": "baseUrl",
"value": "https://api.example.com"
},
{
"enabled": true,
"description": "Access token for the API.",
"name": "token",
"value": "${[access_token]}"
},
{
"enabled": false,
"description": null,
"name": "disabled",
"value": "hello"
}
],
"workspaceId": "CURRENT_WORKSPACE",
"parentId": null,
"parentModel": "environment"
}
]
}
}

View File

@@ -0,0 +1,22 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
import { describe, expect, test } from 'vitest';
import { convertPostmanEnvironment } from '../src';
describe('importer-postman-environment', () => {
const p = path.join(__dirname, 'fixtures');
const fixtures = fs.readdirSync(p);
for (const fixture of fixtures) {
if (fixture.includes('.output')) {
continue;
}
test('Imports ' + fixture, () => {
const contents = fs.readFileSync(path.join(p, fixture), 'utf-8');
const expected = fs.readFileSync(path.join(p, fixture.replace('.input', '.output')), 'utf-8');
const result = convertPostmanEnvironment(contents);
expect(result).toEqual(JSON.parse(expected));
});
}
});

View File

@@ -0,0 +1,3 @@
{
"extends": "../../tsconfig.json"
}

View File

@@ -376,7 +376,7 @@ function toArray<T>(value: unknown): T[] {
/** Recursively render all nested object properties */
function convertTemplateSyntax<T>(obj: T): T {
if (typeof obj === 'string') {
return obj.replace(/{{\s*(_\.)?([^}]+)\s*}}/g, '${[$2]}') as T;
return obj.replace(/{{\s*(_\.)?([^}]*)\s*}}/g, (_m, _dot, expr) => '${[' + expr.trim() + ']}') as T;
} else if (Array.isArray(obj) && obj != null) {
return obj.map(convertTemplateSyntax) as T;
} else if (typeof obj === 'object' && obj != null) {

View File

@@ -1,17 +1,39 @@
import type { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api';
import fs from 'node:fs';
const options = [
{ label: 'ASCII', value: 'ascii' },
{ label: 'UTF-8', value: 'utf8' },
{ label: 'UTF-16 LE', value: 'utf16le' },
{ label: 'Base64', value: 'base64' },
{ label: 'Base64 URL-safe', value: 'base64url' },
{ label: 'Latin-1', value: 'latin1' },
{ label: 'Hexadecimal', value: 'hex' },
];
export const plugin: PluginDefinition = {
templateFunctions: [
{
name: 'fs.readFile',
description: 'Read the contents of a file as utf-8',
args: [{ title: 'Select File', type: 'file', name: 'path', label: 'File' }],
args: [
{ title: 'Select File', type: 'file', name: 'path', label: 'File' },
{
title: 'Select encoding',
type: 'select',
name: 'encoding',
label: 'Encoding',
defaultValue: 'utf8',
description: 'Specifies how the files bytes are decoded into text when read',
options,
},
],
async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
if (!args.values.path) return null;
if (!args.values.path || !args.values.encoding) return null;
try {
return fs.promises.readFile(String(args.values.path ?? ''), 'utf-8');
return fs.promises.readFile(String(args.values.path ?? ''), {
encoding: String(args.values.encoding ?? 'utf-8') as BufferEncoding,
});
} catch {
return null;
}

View File

@@ -3,25 +3,44 @@ import type {
CallTemplateFunctionArgs,
Context,
FormInput,
GetHttpAuthenticationConfigRequest,
HttpResponse,
PluginDefinition,
RenderPurpose,
} from '@yaakapp/api';
import type { DynamicTemplateFunctionArg } from '@yaakapp/api/lib/plugins/TemplateFunctionPlugin';
import { JSONPath } from 'jsonpath-plus';
import { readFileSync } from 'node:fs';
import xpath from 'xpath';
const BEHAVIOR_TTL = 'ttl';
const BEHAVIOR_ALWAYS = 'always';
const BEHAVIOR_SMART = 'smart';
const behaviorArg: FormInput = {
type: 'select',
name: 'behavior',
label: 'Sending Behavior',
defaultValue: 'smart',
options: [
{ label: 'When no responses', value: 'smart' },
{ label: 'Always', value: 'always' },
{ label: 'When no responses', value: BEHAVIOR_SMART },
{ label: 'Always', value: BEHAVIOR_ALWAYS },
{ label: 'When expired', value: BEHAVIOR_TTL },
],
};
const ttlArg: DynamicTemplateFunctionArg = {
type: 'text',
name: 'ttl',
label: 'Expiration Time (seconds)',
placeholder: '0',
description: 'Resend the request when the latest response is older than this many seconds, or if there are no responses yet.',
dynamic(_ctx: Context, { values }: GetHttpAuthenticationConfigRequest) {
const show = values.behavior === BEHAVIOR_TTL;
return { hidden: !show };
},
};
const requestArg: FormInput = {
type: 'http_request',
name: 'request',
@@ -42,6 +61,7 @@ export const plugin: PluginDefinition = {
placeholder: 'Content-Type',
},
behaviorArg,
ttlArg,
],
async onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
if (!args.values.request || !args.values.header) return null;
@@ -50,6 +70,7 @@ export const plugin: PluginDefinition = {
requestId: String(args.values.request || ''),
purpose: args.purpose,
behavior: args.values.behavior ? String(args.values.behavior) : null,
ttl: String(args.values.ttl || ''),
});
if (response == null) return null;
@@ -72,6 +93,7 @@ export const plugin: PluginDefinition = {
placeholder: '$.books[0].id or /books[0]/id',
},
behaviorArg,
ttlArg,
],
async onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
if (!args.values.request || !args.values.path) return null;
@@ -80,6 +102,7 @@ export const plugin: PluginDefinition = {
requestId: String(args.values.request || ''),
purpose: args.purpose,
behavior: args.values.behavior ? String(args.values.behavior) : null,
ttl: String(args.values.ttl || ''),
});
if (response == null) return null;
@@ -113,7 +136,7 @@ export const plugin: PluginDefinition = {
name: 'response.body.raw',
description: 'Access the entire response body, as text',
aliases: ['response'],
args: [requestArg, behaviorArg],
args: [requestArg, behaviorArg, ttlArg],
async onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
if (!args.values.request) return null;
@@ -121,6 +144,7 @@ export const plugin: PluginDefinition = {
requestId: String(args.values.request || ''),
purpose: args.purpose,
behavior: args.values.behavior ? String(args.values.behavior) : null,
ttl: String(args.values.ttl || ''),
});
if (response == null) return null;
@@ -177,9 +201,11 @@ async function getResponse(
requestId,
behavior,
purpose,
ttl,
}: {
requestId: string;
behavior: string | null;
ttl: string | null;
purpose: RenderPurpose;
},
): Promise<HttpResponse | null> {
@@ -203,7 +229,11 @@ async function getResponse(
const finalBehavior = behavior === 'always' && purpose === 'preview' ? 'smart' : behavior;
// Send if no responses and "smart," or "always"
if ((finalBehavior === 'smart' && response == null) || finalBehavior === 'always') {
if (
(finalBehavior === 'smart' && response == null) ||
finalBehavior === 'always' ||
(finalBehavior === BEHAVIOR_TTL && shouldSendExpired(response, ttl))
) {
// NOTE: Render inside this conditional, or we'll get infinite recursion (render->render->...)
const renderedHttpRequest = await ctx.httpRequest.render({ httpRequest, purpose });
response = await ctx.httpRequest.send({ httpRequest: renderedHttpRequest });
@@ -211,3 +241,12 @@ async function getResponse(
return response;
}
function shouldSendExpired(response: HttpResponse | null, ttl: string | null): boolean {
if (response == null) return true;
const ttlSeconds = parseInt(ttl || '0');
if (isNaN(ttlSeconds)) throw new Error(`Invalid TTL "${ttl}"`);
const nowMillis = Date.now();
const respMillis = new Date(response.createdAt + 'Z').getTime();
return respMillis + ttlSeconds * 1000 < nowMillis;
}

299
src-tauri/Cargo.lock generated
View File

@@ -2,15 +2,6 @@
# It is not intended for manual editing.
version = 4
[[package]]
name = "addr2line"
version = "0.24.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
dependencies = [
"gimli",
]
[[package]]
name = "adler2"
version = "2.0.0"
@@ -85,12 +76,6 @@ dependencies = [
"alloc-no-stdlib",
]
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android_log-sys"
version = "0.3.2"
@@ -428,21 +413,6 @@ dependencies = [
"tower-service",
]
[[package]]
name = "backtrace"
version = "0.3.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002"
dependencies = [
"addr2line",
"cfg-if",
"libc",
"miniz_oxide",
"object",
"rustc-demangle",
"windows-targets 0.52.6",
]
[[package]]
name = "base32"
version = "0.5.1"
@@ -717,7 +687,7 @@ dependencies = [
"semver",
"serde",
"serde_json",
"thiserror 2.0.12",
"thiserror 2.0.17",
]
[[package]]
@@ -816,17 +786,16 @@ dependencies = [
[[package]]
name = "chrono"
version = "0.4.41"
version = "0.4.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d"
checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"serde",
"wasm-bindgen",
"windows-link",
"windows-link 0.2.1",
]
[[package]]
@@ -1290,7 +1259,7 @@ dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -2002,12 +1971,6 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "gimli"
version = "0.31.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
[[package]]
name = "gio"
version = "0.18.4"
@@ -2389,9 +2352,9 @@ dependencies = [
[[package]]
name = "hyper-util"
version = "0.1.14"
version = "0.1.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc2fdfdbff08affe55bb779f33b053aa1fe5dd5b54c257343c17edfa55711bdb"
checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8"
dependencies = [
"base64 0.22.1",
"bytes",
@@ -2405,7 +2368,7 @@ dependencies = [
"libc",
"percent-encoding",
"pin-project-lite",
"socket2",
"socket2 0.6.1",
"system-configuration",
"tokio",
"tower-service",
@@ -3205,7 +3168,7 @@ dependencies = [
"once_cell",
"png",
"serde",
"thiserror 2.0.12",
"thiserror 2.0.17",
"windows-sys 0.59.0",
]
@@ -3628,15 +3591,6 @@ dependencies = [
"objc2-security",
]
[[package]]
name = "object"
version = "0.36.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
dependencies = [
"memchr",
]
[[package]]
name = "once_cell"
version = "1.21.3"
@@ -3782,7 +3736,7 @@ dependencies = [
"objc2-osa-kit",
"serde",
"serde_json",
"thiserror 2.0.12",
"thiserror 2.0.17",
]
[[package]]
@@ -4334,8 +4288,8 @@ dependencies = [
"quinn-udp",
"rustc-hash",
"rustls",
"socket2",
"thiserror 2.0.12",
"socket2 0.5.10",
"thiserror 2.0.17",
"tokio",
"tracing",
"web-time",
@@ -4356,7 +4310,7 @@ dependencies = [
"rustls",
"rustls-pki-types",
"slab",
"thiserror 2.0.12",
"thiserror 2.0.17",
"tinyvec",
"tracing",
"web-time",
@@ -4371,7 +4325,7 @@ dependencies = [
"cfg_aliases",
"libc",
"once_cell",
"socket2",
"socket2 0.5.10",
"tracing",
"windows-sys 0.59.0",
]
@@ -4552,7 +4506,7 @@ checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b"
dependencies = [
"getrandom 0.2.16",
"libredox",
"thiserror 2.0.12",
"thiserror 2.0.17",
]
[[package]]
@@ -4766,12 +4720,6 @@ dependencies = [
"serde_json",
]
[[package]]
name = "rustc-demangle"
version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
[[package]]
name = "rustc-hash"
version = "2.1.1"
@@ -4815,9 +4763,9 @@ dependencies = [
[[package]]
name = "rustls"
version = "0.23.27"
version = "0.23.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321"
checksum = "751e04a496ca00bb97a5e043158d23d66b5aabf2e1d5aa2a0aaebb1aafe6f82c"
dependencies = [
"once_cell",
"ring",
@@ -4851,9 +4799,9 @@ dependencies = [
[[package]]
name = "rustls-platform-verifier"
version = "0.6.0"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eda84358ed17f1f354cf4b1909ad346e6c7bc2513e8c40eb08e0157aa13a9070"
checksum = "be59af91596cac372a6942530653ad0c3a246cdd491aaa9dcaee47f88d67d5a0"
dependencies = [
"core-foundation 0.10.1",
"core-foundation-sys",
@@ -4878,9 +4826,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
[[package]]
name = "rustls-webpki"
version = "0.103.3"
version = "0.103.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435"
checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf"
dependencies = [
"ring",
"rustls-pki-types",
@@ -4981,7 +4929,7 @@ dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
"thiserror 2.0.12",
"thiserror 2.0.17",
]
[[package]]
@@ -5065,9 +5013,9 @@ dependencies = [
[[package]]
name = "serde"
version = "1.0.226"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dca6411025b24b60bfa7ec1fe1f8e710ac09782dca409ee8237ba74b51295fd"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
@@ -5107,18 +5055,18 @@ dependencies = [
[[package]]
name = "serde_core"
version = "1.0.226"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba2ba63999edb9dac981fb34b3e5c0d111a69b0924e253ed29d83f7c99e966a4"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.226"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8db53ae22f34573731bafa1db20f04027b2d25e02d8205921b569171699cdb33"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
@@ -5138,14 +5086,15 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.140"
version = "1.0.145"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
"serde_core",
]
[[package]]
@@ -5392,6 +5341,16 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "socket2"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881"
dependencies = [
"libc",
"windows-sys 0.60.2",
]
[[package]]
name = "softbuffer"
version = "0.4.6"
@@ -5587,9 +5546,9 @@ dependencies = [
[[package]]
name = "tao"
version = "0.34.3"
version = "0.34.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "959469667dbcea91e5485fc48ba7dd6023face91bb0f1a14681a70f99847c3f7"
checksum = "f3a753bdc39c07b192151523a3f77cd0394aa75413802c883a0f6f6a0e5ee2e7"
dependencies = [
"bitflags 2.9.1",
"block2 0.6.1",
@@ -5661,9 +5620,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
[[package]]
name = "tauri"
version = "2.8.5"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4d1d3b3dc4c101ac989fd7db77e045cc6d91a25349cd410455cb5c57d510c1c"
checksum = "7f07c6590706b2fc0ab287b041cf5ce9c435b3850bdae5571e19d9d27584e89d"
dependencies = [
"anyhow",
"bytes",
@@ -5701,7 +5660,7 @@ dependencies = [
"tauri-runtime",
"tauri-runtime-wry",
"tauri-utils",
"thiserror 2.0.12",
"thiserror 2.0.17",
"tokio",
"tray-icon",
"url",
@@ -5714,9 +5673,9 @@ dependencies = [
[[package]]
name = "tauri-build"
version = "2.4.1"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c432ccc9ff661803dab74c6cd78de11026a578a9307610bbc39d3c55be7943f"
checksum = "f71be1f494b683ac439e6d61c16ab5c472c6f9c6ee78995b29556d9067c021a1"
dependencies = [
"anyhow",
"cargo_toml",
@@ -5736,9 +5695,9 @@ dependencies = [
[[package]]
name = "tauri-codegen"
version = "2.4.0"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ab3a62cf2e6253936a8b267c2e95839674e7439f104fa96ad0025e149d54d8a"
checksum = "6c1fe64c74cc40f90848281a90058a6db931eb400b60205840e09801ee30f190"
dependencies = [
"base64 0.22.1",
"brotli",
@@ -5754,7 +5713,7 @@ dependencies = [
"sha2",
"syn 2.0.101",
"tauri-utils",
"thiserror 2.0.12",
"thiserror 2.0.17",
"time",
"url",
"uuid",
@@ -5763,9 +5722,9 @@ dependencies = [
[[package]]
name = "tauri-macros"
version = "2.4.0"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4368ea8094e7045217edb690f493b55b30caf9f3e61f79b4c24b6db91f07995e"
checksum = "260c5d2eb036b76206b9fca20b7be3614cfd21046c5396f7959e0e64a4b07f2f"
dependencies = [
"heck 0.5.0",
"proc-macro2",
@@ -5777,9 +5736,9 @@ dependencies = [
[[package]]
name = "tauri-plugin"
version = "2.4.0"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9946a3cede302eac0c6eb6c6070ac47b1768e326092d32efbb91f21ed58d978f"
checksum = "3d7ce9aab979296b2f91e6fbf154207c2e3512b12ddca0b24bfa0e0cde6b2976"
dependencies = [
"anyhow",
"glob",
@@ -5804,7 +5763,7 @@ dependencies = [
"serde_json",
"tauri",
"tauri-plugin",
"thiserror 2.0.12",
"thiserror 2.0.17",
]
[[package]]
@@ -5821,7 +5780,7 @@ dependencies = [
"tauri",
"tauri-plugin",
"tauri-utils",
"thiserror 2.0.12",
"thiserror 2.0.17",
"tracing",
"url",
"windows-registry",
@@ -5842,7 +5801,7 @@ dependencies = [
"tauri",
"tauri-plugin",
"tauri-plugin-fs",
"thiserror 2.0.12",
"thiserror 2.0.17",
"url",
]
@@ -5863,7 +5822,7 @@ dependencies = [
"tauri",
"tauri-plugin",
"tauri-utils",
"thiserror 2.0.12",
"thiserror 2.0.17",
"toml 0.9.5",
"url",
]
@@ -5886,7 +5845,7 @@ dependencies = [
"swift-rs",
"tauri",
"tauri-plugin",
"thiserror 2.0.12",
"thiserror 2.0.17",
"time",
]
@@ -5906,7 +5865,7 @@ dependencies = [
"serde_json",
"tauri",
"tauri-plugin",
"thiserror 2.0.12",
"thiserror 2.0.17",
"url",
"windows",
"zbus",
@@ -5927,7 +5886,7 @@ dependencies = [
"sys-locale",
"tauri",
"tauri-plugin",
"thiserror 2.0.12",
"thiserror 2.0.17",
]
[[package]]
@@ -5947,7 +5906,7 @@ dependencies = [
"shared_child",
"tauri",
"tauri-plugin",
"thiserror 2.0.12",
"thiserror 2.0.17",
"tokio",
]
@@ -5961,7 +5920,7 @@ dependencies = [
"serde_json",
"tauri",
"tauri-plugin-deep-link",
"thiserror 2.0.12",
"thiserror 2.0.17",
"tracing",
"windows-sys 0.60.2",
"zbus",
@@ -5991,7 +5950,7 @@ dependencies = [
"tauri",
"tauri-plugin",
"tempfile",
"thiserror 2.0.12",
"thiserror 2.0.17",
"time",
"tokio",
"url",
@@ -6011,14 +5970,14 @@ dependencies = [
"serde_json",
"tauri",
"tauri-plugin",
"thiserror 2.0.12",
"thiserror 2.0.17",
]
[[package]]
name = "tauri-runtime"
version = "2.8.0"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4cfc9ad45b487d3fded5a4731a567872a4812e9552e3964161b08edabf93846"
checksum = "3367f0b47df90e9195cd9f04a56b0055a2cba45aa11923c6c253d748778176fc"
dependencies = [
"cookie",
"dpi",
@@ -6032,7 +5991,7 @@ dependencies = [
"serde",
"serde_json",
"tauri-utils",
"thiserror 2.0.12",
"thiserror 2.0.17",
"url",
"webkit2gtk",
"webview2-com",
@@ -6041,9 +6000,9 @@ dependencies = [
[[package]]
name = "tauri-runtime-wry"
version = "2.8.1"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1fe9d48bd122ff002064e88cfcd7027090d789c4302714e68fcccba0f4b7807"
checksum = "80d91d29ca680c545364cf75ba2f2e3c7ea2ab6376bfa3be26b56fa2463a5b5e"
dependencies = [
"gtk",
"http",
@@ -6068,9 +6027,9 @@ dependencies = [
[[package]]
name = "tauri-utils"
version = "2.7.0"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41a3852fdf9a4f8fbeaa63dc3e9a85284dd6ef7200751f0bd66ceee30c93f212"
checksum = "f6b8bbe426abdbf52d050e52ed693130dbd68375b9ad82a3fb17efb4c8d85673"
dependencies = [
"anyhow",
"brotli",
@@ -6096,7 +6055,7 @@ dependencies = [
"serde_json",
"serde_with",
"swift-rs",
"thiserror 2.0.12",
"thiserror 2.0.17",
"toml 0.9.5",
"url",
"urlpattern",
@@ -6159,11 +6118,11 @@ dependencies = [
[[package]]
name = "thiserror"
version = "2.0.12"
version = "2.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8"
dependencies = [
"thiserror-impl 2.0.12",
"thiserror-impl 2.0.17",
]
[[package]]
@@ -6179,9 +6138,9 @@ dependencies = [
[[package]]
name = "thiserror-impl"
version = "2.0.12"
version = "2.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
dependencies = [
"proc-macro2",
"quote",
@@ -6268,27 +6227,26 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.45.1"
version = "1.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779"
checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408"
dependencies = [
"backtrace",
"bytes",
"libc",
"mio",
"pin-project-lite",
"signal-hook-registry",
"socket2",
"socket2 0.6.1",
"tokio-macros",
"tracing",
"windows-sys 0.52.0",
"windows-sys 0.61.2",
]
[[package]]
name = "tokio-macros"
version = "2.5.0"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
dependencies = [
"proc-macro2",
"quote",
@@ -6478,7 +6436,7 @@ dependencies = [
"percent-encoding",
"pin-project",
"prost",
"socket2",
"socket2 0.5.10",
"tokio",
"tokio-stream",
"tower 0.4.13",
@@ -6614,7 +6572,7 @@ dependencies = [
"once_cell",
"png",
"serde",
"thiserror 2.0.12",
"thiserror 2.0.17",
"windows-sys 0.59.0",
]
@@ -6645,21 +6603,21 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "ts-rs"
version = "11.0.1"
version = "11.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ef1b7a6d914a34127ed8e1fa927eb7088903787bcded4fa3eef8f85ee1568be"
checksum = "4994acea2522cd2b3b85c1d9529a55991e3ad5e25cdcd3de9d505972c4379424"
dependencies = [
"chrono",
"serde_json",
"thiserror 2.0.12",
"thiserror 2.0.17",
"ts-rs-macros",
]
[[package]]
name = "ts-rs-macros"
version = "11.0.1"
version = "11.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9d4ed7b4c18cc150a6a0a1e9ea1ecfa688791220781af6e119f9599a8502a0a"
checksum = "ee6ff59666c9cbaec3533964505d39154dc4e0a56151fdea30a09ed0301f62e2"
dependencies = [
"proc-macro2",
"quote",
@@ -6682,7 +6640,7 @@ dependencies = [
"rustls",
"rustls-pki-types",
"sha1",
"thiserror 2.0.12",
"thiserror 2.0.17",
"utf-8",
]
@@ -7204,7 +7162,7 @@ version = "0.38.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36695906a1b53a3bf5c4289621efedac12b73eeb0b89e7e1a89b517302d5d75c"
dependencies = [
"thiserror 2.0.12",
"thiserror 2.0.17",
"windows",
"windows-core",
]
@@ -7270,7 +7228,7 @@ dependencies = [
"windows-collections",
"windows-core",
"windows-future",
"windows-link",
"windows-link 0.1.1",
"windows-numerics",
]
@@ -7291,7 +7249,7 @@ checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link",
"windows-link 0.1.1",
"windows-result",
"windows-strings",
]
@@ -7303,7 +7261,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e"
dependencies = [
"windows-core",
"windows-link",
"windows-link 0.1.1",
"windows-threading",
]
@@ -7335,6 +7293,12 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-numerics"
version = "0.2.0"
@@ -7342,7 +7306,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
dependencies = [
"windows-core",
"windows-link",
"windows-link 0.1.1",
]
[[package]]
@@ -7351,7 +7315,7 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3bab093bdd303a1240bb99b8aba8ea8a69ee19d34c9e2ef9594e708a4878820"
dependencies = [
"windows-link",
"windows-link 0.1.1",
"windows-result",
"windows-strings",
]
@@ -7362,7 +7326,7 @@ version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
dependencies = [
"windows-link",
"windows-link 0.1.1",
]
[[package]]
@@ -7371,7 +7335,7 @@ version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
dependencies = [
"windows-link",
"windows-link 0.1.1",
]
[[package]]
@@ -7410,6 +7374,15 @@ dependencies = [
"windows-targets 0.53.2",
]
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link 0.2.1",
]
[[package]]
name = "windows-targets"
version = "0.42.2"
@@ -7478,7 +7451,7 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6"
dependencies = [
"windows-link",
"windows-link 0.1.1",
]
[[package]]
@@ -7487,7 +7460,7 @@ version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e04a5c6627e310a23ad2358483286c7df260c964eb2d003d8efd6d0f4e79265c"
dependencies = [
"windows-link",
"windows-link 0.1.1",
]
[[package]]
@@ -7718,7 +7691,7 @@ dependencies = [
"os_pipe",
"rustix 0.38.44",
"tempfile",
"thiserror 2.0.12",
"thiserror 2.0.17",
"tree_magic_mini",
"wayland-backend",
"wayland-client",
@@ -7734,9 +7707,9 @@ checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb"
[[package]]
name = "wry"
version = "0.53.3"
version = "0.53.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31f0e9642a0d061f6236c54ccae64c2722a7879ad4ec7dff59bd376d446d8e90"
checksum = "6d78ec082b80fa088569a970d043bb3050abaabf4454101d44514ee8d9a8c9f6"
dependencies = [
"base64 0.22.1",
"block2 0.6.1",
@@ -7766,7 +7739,7 @@ dependencies = [
"sha2",
"soup3",
"tao-macros",
"thiserror 2.0.12",
"thiserror 2.0.17",
"url",
"webkit2gtk",
"webkit2gtk-sys",
@@ -7843,6 +7816,7 @@ dependencies = [
"cookie",
"eventsource-client",
"http",
"hyper-util",
"log",
"md5 0.8.0",
"mime_guess",
@@ -7865,9 +7839,10 @@ dependencies = [
"tauri-plugin-single-instance",
"tauri-plugin-updater",
"tauri-plugin-window-state",
"thiserror 2.0.12",
"thiserror 2.0.17",
"tokio",
"tokio-stream",
"tower-service",
"ts-rs",
"uuid",
"yaak-common",
@@ -7894,7 +7869,7 @@ dependencies = [
"reqwest",
"serde",
"tauri",
"thiserror 2.0.12",
"thiserror 2.0.17",
]
[[package]]
@@ -7909,7 +7884,7 @@ dependencies = [
"serde",
"tauri",
"tauri-plugin",
"thiserror 2.0.12",
"thiserror 2.0.17",
"yaak-models",
]
@@ -7921,7 +7896,7 @@ dependencies = [
"serde",
"tauri",
"tauri-plugin",
"thiserror 2.0.12",
"thiserror 2.0.17",
"ts-rs",
]
@@ -7937,7 +7912,7 @@ dependencies = [
"serde_yaml",
"tauri",
"tauri-plugin",
"thiserror 2.0.12",
"thiserror 2.0.17",
"ts-rs",
"yaak-models",
"yaak-sync",
@@ -7991,7 +7966,7 @@ dependencies = [
"serde_json",
"tauri",
"tauri-plugin",
"thiserror 2.0.12",
"thiserror 2.0.17",
"ts-rs",
"yaak-common",
"yaak-models",
@@ -8030,9 +8005,9 @@ dependencies = [
"tauri",
"tauri-plugin",
"tauri-plugin-dialog",
"thiserror 2.0.12",
"tokio",
"thiserror 2.0.17",
"ts-rs",
"yaak-common",
]
[[package]]
@@ -8057,7 +8032,7 @@ dependencies = [
"tauri",
"tauri-plugin",
"tauri-plugin-shell",
"thiserror 2.0.12",
"thiserror 2.0.17",
"tokio",
"tokio-tungstenite",
"ts-rs",
@@ -8091,7 +8066,7 @@ dependencies = [
"sha1",
"tauri",
"tauri-plugin",
"thiserror 2.0.12",
"thiserror 2.0.17",
"tokio",
"ts-rs",
"yaak-models",
@@ -8106,7 +8081,7 @@ dependencies = [
"serde",
"serde-wasm-bindgen",
"serde_json",
"thiserror 2.0.12",
"thiserror 2.0.17",
"tokio",
"ts-rs",
"wasm-bindgen",
@@ -8124,7 +8099,7 @@ dependencies = [
"serde_json",
"tauri",
"tauri-plugin",
"thiserror 2.0.12",
"thiserror 2.0.17",
"tokio",
"tokio-tungstenite",
"yaak-http",
@@ -8345,7 +8320,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aed5f10c571472911e37d8f7601a8dfba52b4f7f73a344015291b82ab292faf6"
dependencies = [
"log",
"thiserror 2.0.12",
"thiserror 2.0.17",
"zip",
]

View File

@@ -37,7 +37,7 @@ updater = []
license = ["yaak-license"]
[build-dependencies]
tauri-build = { version = "2.4.1", features = [] }
tauri-build = { version = "2.5.0", features = [] }
[target.'cfg(target_os = "linux")'.dependencies]
openssl-sys = { version = "0.9.105", features = ["vendored"] } # For Ubuntu installation to work
@@ -68,6 +68,8 @@ tauri-plugin-shell = { workspace = true }
tauri-plugin-single-instance = { version = "2.3.4", features = ["deep-link"] }
tauri-plugin-updater = "2.9.0"
tauri-plugin-window-state = "2.4.0"
hyper-util = { version = "0.1.17", default-features = false, features = ["client-legacy"] }
tower-service = "0.3.3"
thiserror = { workspace = true }
tokio = { workspace = true, features = ["sync"] }
tokio-stream = "0.1.17"
@@ -89,23 +91,23 @@ yaak-templates = { workspace = true }
yaak-ws = { path = "yaak-ws" }
[workspace.dependencies]
chrono = "0.4.41"
chrono = "0.4.42"
hex = "0.4.3"
keyring = "3.6.3"
reqwest = "0.12.20"
reqwest_cookie_store = "0.8.0"
rustls = { version = "0.23.27", default-features = false }
rustls-platform-verifier = "0.6.0"
serde = "1.0.219"
serde_json = "1.0.140"
rustls = { version = "0.23.33", default-features = false }
rustls-platform-verifier = "0.6.1"
serde = "1.0.228"
serde_json = "1.0.145"
sha2 = "0.10.9"
tauri = "2.8.5"
tauri-plugin = "2.4.0"
tauri = "2.9.0"
tauri-plugin = "2.5.0"
tauri-plugin-dialog = "2.4.0"
tauri-plugin-shell = "2.3.1"
thiserror = "2.0.12"
tokio = "1.45.1"
ts-rs = "11.0.1"
thiserror = "2.0.17"
tokio = "1.48.0"
ts-rs = "11.1.0"
yaak-common = { path = "yaak-common" }
yaak-crypto = { path = "yaak-crypto" }
yaak-fonts = { path = "yaak-fonts" }

54
src-tauri/src/dns.rs Normal file
View File

@@ -0,0 +1,54 @@
use hyper_util::client::legacy::connect::dns::{
GaiResolver as HyperGaiResolver, Name as HyperName,
};
use reqwest::dns::{Addrs, Name, Resolve, Resolving};
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
use std::str::FromStr;
use std::sync::Arc;
use tower_service::Service;
#[derive(Clone)]
pub(crate) struct LocalhostResolver {
fallback: HyperGaiResolver,
}
impl LocalhostResolver {
pub fn new() -> Arc<Self> {
let resolver = HyperGaiResolver::new();
Arc::new(Self { fallback: resolver })
}
}
impl Resolve for LocalhostResolver {
fn resolve(&self, name: Name) -> Resolving {
let host = name.as_str().to_lowercase();
let is_localhost = host.ends_with(".localhost");
if is_localhost {
// Port 0 is fine; reqwest replaces it with the URL's explicit
// port or the schemes default (80/443, etc.).
// (See docs note below.)
let addrs: Vec<SocketAddr> = vec![
SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0),
SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 0),
];
return Box::pin(async move {
Ok::<Addrs, Box<dyn std::error::Error + Send + Sync>>(Box::new(addrs.into_iter()))
});
}
let mut fallback = self.fallback.clone();
let name_str = name.as_str().to_string();
Box::pin(async move {
match HyperName::from_str(&name_str) {
Ok(n) => fallback
.call(n)
.await
.map(|addrs| Box::new(addrs) as Addrs)
.map_err(|err| Box::new(err) as Box<dyn std::error::Error + Send + Sync>),
Err(e) => Err(Box::new(e) as Box<dyn std::error::Error + Send + Sync>),
}
})
}
}

View File

@@ -1,48 +1,74 @@
use chrono::{NaiveDateTime, Utc};
use log::debug;
use std::sync::OnceLock;
use tauri::{AppHandle, Runtime};
use yaak_models::query_manager::QueryManagerExt;
use yaak_models::util::UpdateSource;
const NAMESPACE: &str = "analytics";
const NUM_LAUNCHES_KEY: &str = "num_launches";
const LAST_VERSION_KEY: &str = "last_tracked_version";
const PREV_VERSION_KEY: &str = "last_tracked_version_prev";
const VERSION_SINCE_KEY: &str = "last_tracked_version_since";
#[derive(Default, Debug)]
#[derive(Default, Debug, Clone)]
pub struct LaunchEventInfo {
pub current_version: String,
pub previous_version: String,
pub launched_after_update: bool,
pub version_since: NaiveDateTime,
pub user_since: NaiveDateTime,
pub num_launches: i32,
}
pub async fn store_launch_history<R: Runtime>(app_handle: &AppHandle<R>) -> LaunchEventInfo {
let last_tracked_version_key = "last_tracked_version";
static LAUNCH_INFO: OnceLock<LaunchEventInfo> = OnceLock::new();
let mut info = LaunchEventInfo::default();
pub fn get_or_upsert_launch_info<R: Runtime>(app_handle: &AppHandle<R>) -> &LaunchEventInfo {
LAUNCH_INFO.get_or_init(|| {
let now = Utc::now().naive_utc();
let mut info = LaunchEventInfo {
version_since: app_handle.db().get_key_value_dte(NAMESPACE, VERSION_SINCE_KEY, now),
current_version: app_handle.package_info().version.to_string(),
user_since: app_handle.db().get_settings().created_at,
num_launches: app_handle.db().get_key_value_int(NAMESPACE, NUM_LAUNCHES_KEY, 0) + 1,
info.num_launches = get_num_launches(app_handle).await + 1;
info.current_version = app_handle.package_info().version.to_string();
// The rest will be set below
..Default::default()
};
app_handle
.with_tx(|tx| {
info.previous_version =
tx.get_key_value_string(NAMESPACE, last_tracked_version_key, "");
app_handle
.with_tx(|tx| {
// Load the previously tracked version
let curr_db = tx.get_key_value_str(NAMESPACE, LAST_VERSION_KEY, "");
let prev_db = tx.get_key_value_str(NAMESPACE, PREV_VERSION_KEY, "");
if !info.previous_version.is_empty() {
info.launched_after_update = info.current_version != info.previous_version;
};
// We just updated if the app version is different from the last tracked version we stored
if !curr_db.is_empty() && info.current_version != curr_db {
info.launched_after_update = true;
}
// Update key values
// If we just updated, track the previous version as the "previous" current version
if info.launched_after_update {
info.previous_version = curr_db.clone();
info.version_since = now;
} else {
info.previous_version = prev_db.clone();
}
let source = &UpdateSource::Background;
let version = info.current_version.as_str();
tx.set_key_value_string(NAMESPACE, last_tracked_version_key, version, source);
tx.set_key_value_int(NAMESPACE, NUM_LAUNCHES_KEY, info.num_launches, source);
Ok(())
})
.unwrap();
// Rotate stored versions: move previous into the "prev" slot before overwriting
let source = &UpdateSource::Background;
info
}
pub async fn get_num_launches<R: Runtime>(app_handle: &AppHandle<R>) -> i32 {
app_handle.db().get_key_value_int(NAMESPACE, NUM_LAUNCHES_KEY, 0)
tx.set_key_value_str(NAMESPACE, PREV_VERSION_KEY, &info.previous_version, source);
tx.set_key_value_str(NAMESPACE, LAST_VERSION_KEY, &info.current_version, source);
tx.set_key_value_dte(NAMESPACE, VERSION_SINCE_KEY, info.version_since, source);
tx.set_key_value_int(NAMESPACE, NUM_LAUNCHES_KEY, info.num_launches, source);
Ok(())
})
.unwrap();
debug!("Initialized launch info");
info
})
}

View File

@@ -33,6 +33,7 @@ use yaak_plugins::events::{
use yaak_plugins::manager::PluginManager;
use yaak_plugins::template_callback::PluginTemplateCallback;
use yaak_templates::{RenderErrorBehavior, RenderOptions};
use crate::dns::LocalhostResolver;
pub async fn send_http_request<R: Runtime>(
window: &WebviewWindow<R>,
@@ -110,6 +111,7 @@ pub async fn send_http_request<R: Runtime>(
.gzip(true)
.brotli(true)
.deflate(true)
.dns_resolver(LocalhostResolver::new())
.referer(false)
.tls_info(true);

View File

@@ -28,7 +28,7 @@ pub(crate) async fn import_data<R: Runtime>(
.workspaces
.into_iter()
.map(|mut v| {
v.id = maybe_gen_id::<Workspace>(v.id.as_str(), &mut id_map);
v.id = maybe_gen_id::<Workspace, R>(window, v.id.as_str(), &mut id_map);
v
})
.collect();
@@ -37,11 +37,12 @@ pub(crate) async fn import_data<R: Runtime>(
.environments
.into_iter()
.map(|mut v| {
v.id = maybe_gen_id::<Environment>(v.id.as_str(), &mut id_map);
v.workspace_id = maybe_gen_id::<Workspace>(v.workspace_id.as_str(), &mut id_map);
v.id = maybe_gen_id::<Environment, R>(window, v.id.as_str(), &mut id_map);
v.workspace_id =
maybe_gen_id::<Workspace, R>(window, v.workspace_id.as_str(), &mut id_map);
match (v.parent_model.as_str(), v.parent_id.clone().as_deref()) {
("folder", Some(parent_id)) => {
v.parent_id = Some(maybe_gen_id::<Folder>(&parent_id, &mut id_map));
v.parent_id = Some(maybe_gen_id::<Folder, R>(window, &parent_id, &mut id_map));
}
("", _) => {
// Fix any empty ones
@@ -60,9 +61,10 @@ pub(crate) async fn import_data<R: Runtime>(
.folders
.into_iter()
.map(|mut v| {
v.id = maybe_gen_id::<Folder>(v.id.as_str(), &mut id_map);
v.workspace_id = maybe_gen_id::<Workspace>(v.workspace_id.as_str(), &mut id_map);
v.folder_id = maybe_gen_id_opt::<Folder>(v.folder_id, &mut id_map);
v.id = maybe_gen_id::<Folder, R>(window, v.id.as_str(), &mut id_map);
v.workspace_id =
maybe_gen_id::<Workspace, R>(window, v.workspace_id.as_str(), &mut id_map);
v.folder_id = maybe_gen_id_opt::<Folder, R>(window, v.folder_id, &mut id_map);
v
})
.collect();
@@ -71,9 +73,10 @@ pub(crate) async fn import_data<R: Runtime>(
.http_requests
.into_iter()
.map(|mut v| {
v.id = maybe_gen_id::<HttpRequest>(v.id.as_str(), &mut id_map);
v.workspace_id = maybe_gen_id::<Workspace>(v.workspace_id.as_str(), &mut id_map);
v.folder_id = maybe_gen_id_opt::<Folder>(v.folder_id, &mut id_map);
v.id = maybe_gen_id::<HttpRequest, R>(window, v.id.as_str(), &mut id_map);
v.workspace_id =
maybe_gen_id::<Workspace, R>(window, v.workspace_id.as_str(), &mut id_map);
v.folder_id = maybe_gen_id_opt::<Folder, R>(window, v.folder_id, &mut id_map);
v
})
.collect();
@@ -82,9 +85,10 @@ pub(crate) async fn import_data<R: Runtime>(
.grpc_requests
.into_iter()
.map(|mut v| {
v.id = maybe_gen_id::<GrpcRequest>(v.id.as_str(), &mut id_map);
v.workspace_id = maybe_gen_id::<Workspace>(v.workspace_id.as_str(), &mut id_map);
v.folder_id = maybe_gen_id_opt::<Folder>(v.folder_id, &mut id_map);
v.id = maybe_gen_id::<GrpcRequest, R>(window, v.id.as_str(), &mut id_map);
v.workspace_id =
maybe_gen_id::<Workspace, R>(window, v.workspace_id.as_str(), &mut id_map);
v.folder_id = maybe_gen_id_opt::<Folder, R>(window, v.folder_id, &mut id_map);
v
})
.collect();
@@ -93,9 +97,10 @@ pub(crate) async fn import_data<R: Runtime>(
.websocket_requests
.into_iter()
.map(|mut v| {
v.id = maybe_gen_id::<WebsocketRequest>(v.id.as_str(), &mut id_map);
v.workspace_id = maybe_gen_id::<Workspace>(v.workspace_id.as_str(), &mut id_map);
v.folder_id = maybe_gen_id_opt::<Folder>(v.folder_id, &mut id_map);
v.id = maybe_gen_id::<WebsocketRequest, R>(window, v.id.as_str(), &mut id_map);
v.workspace_id =
maybe_gen_id::<Workspace, R>(window, v.workspace_id.as_str(), &mut id_map);
v.folder_id = maybe_gen_id_opt::<Folder, R>(window, v.folder_id, &mut id_map);
v
})
.collect();

View File

@@ -38,19 +38,14 @@ use yaak_models::models::{
};
use yaak_models::query_manager::QueryManagerExt;
use yaak_models::util::{BatchUpsertResult, UpdateSource, get_workspace_export_resources};
use yaak_plugins::events::{
CallGrpcRequestActionArgs, CallGrpcRequestActionRequest, CallHttpRequestActionArgs,
CallHttpRequestActionRequest, Color, FilterResponse, GetGrpcRequestActionsResponse,
GetHttpAuthenticationConfigResponse, GetHttpAuthenticationSummaryResponse,
GetHttpRequestActionsResponse, GetTemplateFunctionsResponse, InternalEvent,
InternalEventPayload, JsonPrimitive, PluginWindowContext, RenderPurpose, ShowToastRequest,
};
use yaak_plugins::events::{CallGrpcRequestActionArgs, CallGrpcRequestActionRequest, CallHttpRequestActionArgs, CallHttpRequestActionRequest, Color, FilterResponse, GetGrpcRequestActionsResponse, GetHttpAuthenticationConfigResponse, GetHttpAuthenticationSummaryResponse, GetHttpRequestActionsResponse, GetTemplateFunctionSummaryResponse, GetTemplateFunctionConfigResponse, InternalEvent, InternalEventPayload, JsonPrimitive, PluginWindowContext, RenderPurpose, ShowToastRequest};
use yaak_plugins::manager::PluginManager;
use yaak_plugins::plugin_meta::PluginMetadata;
use yaak_plugins::template_callback::PluginTemplateCallback;
use yaak_sse::sse::ServerSentEvent;
use yaak_templates::format::format_json;
use yaak_templates::{RenderErrorBehavior, RenderOptions, Tokens, transform_args};
use yaak_templates::format_xml::format_xml;
mod commands;
mod encoding;
@@ -66,6 +61,7 @@ mod updates;
mod uri_scheme;
mod window;
mod window_menu;
mod dns;
#[derive(serde::Serialize)]
#[serde(default, rename_all = "camelCase")]
@@ -744,6 +740,11 @@ async fn cmd_format_json(text: &str) -> YaakResult<String> {
Ok(format_json(text, " "))
}
#[tauri::command]
async fn cmd_format_xml(text: &str) -> YaakResult<String> {
Ok(format_xml(text, " "))
}
#[tauri::command]
async fn cmd_http_response_body<R: Runtime>(
window: WebviewWindow<R>,
@@ -827,11 +828,36 @@ async fn cmd_grpc_request_actions<R: Runtime>(
}
#[tauri::command]
async fn cmd_template_functions<R: Runtime>(
async fn cmd_template_function_summaries<R: Runtime>(
window: WebviewWindow<R>,
plugin_manager: State<'_, PluginManager>,
) -> YaakResult<Vec<GetTemplateFunctionsResponse>> {
Ok(plugin_manager.get_template_functions(&window).await?)
) -> YaakResult<Vec<GetTemplateFunctionSummaryResponse>> {
let results = plugin_manager.get_template_function_summaries(&window).await?;
Ok(results)
}
#[tauri::command]
async fn cmd_template_function_config<R: Runtime>(
window: WebviewWindow<R>,
plugin_manager: State<'_, PluginManager>,
function_name: &str,
values: HashMap<String, JsonPrimitive>,
model: AnyModel,
environment_id: Option<&str>,
) -> YaakResult<GetTemplateFunctionConfigResponse> {
let (workspace_id, folder_id) = match model.clone() {
AnyModel::HttpRequest(m) => (m.workspace_id, m.folder_id),
AnyModel::GrpcRequest(m) => (m.workspace_id, m.folder_id),
AnyModel::WebsocketRequest(m) => (m.workspace_id, m.folder_id),
AnyModel::Folder(m) => (m.workspace_id, m.folder_id),
AnyModel::Workspace(m) => (m.id, None),
m => {
return Err(GenericError(format!("Unsupported model to call template functions {m:?}")));
}
};
let environment_chain =
window.db().resolve_environments(&workspace_id, folder_id.as_deref(), environment_id)?;
Ok(plugin_manager.get_template_function_config(&window, function_name, environment_chain, values, model.id()).await?)
}
#[tauri::command]
@@ -849,10 +875,10 @@ async fn cmd_get_http_authentication_config<R: Runtime>(
plugin_manager: State<'_, PluginManager>,
auth_name: &str,
values: HashMap<String, JsonPrimitive>,
request: AnyModel,
model: AnyModel,
environment_id: Option<&str>,
) -> YaakResult<GetHttpAuthenticationConfigResponse> {
let (workspace_id, folder_id) = match request.clone() {
let (workspace_id, folder_id) = match model.clone() {
AnyModel::HttpRequest(m) => (m.workspace_id, m.folder_id),
AnyModel::GrpcRequest(m) => (m.workspace_id, m.folder_id),
AnyModel::WebsocketRequest(m) => (m.workspace_id, m.folder_id),
@@ -867,7 +893,7 @@ async fn cmd_get_http_authentication_config<R: Runtime>(
window.db().resolve_environments(&workspace_id, folder_id.as_deref(), environment_id)?;
Ok(plugin_manager
.get_http_authentication_config(&window, environment_chain, auth_name, values, request.id())
.get_http_authentication_config(&window, environment_chain, auth_name, values, model.id())
.await?)
}
@@ -1007,6 +1033,35 @@ async fn cmd_save_response<R: Runtime>(
Ok(())
}
#[tauri::command]
async fn cmd_send_folder<R: Runtime>(
app_handle: AppHandle<R>,
window: WebviewWindow<R>,
environment_id: Option<String>,
cookie_jar_id: Option<String>,
folder_id: &str,
) -> YaakResult<()> {
let requests = app_handle.db().list_http_requests_for_folder_recursive(folder_id)?;
for request in requests {
let app_handle = app_handle.clone();
let window = window.clone();
let environment_id = environment_id.clone();
let cookie_jar_id = cookie_jar_id.clone();
tokio::spawn(async move {
let _ = cmd_send_http_request(
app_handle,
window,
environment_id.as_deref(),
cookie_jar_id.as_deref(),
request,
)
.await;
});
}
Ok(())
}
#[tauri::command]
async fn cmd_send_http_request<R: Runtime>(
app_handle: AppHandle<R>,
@@ -1366,6 +1421,7 @@ pub fn run() {
cmd_export_data,
cmd_http_response_body,
cmd_format_json,
cmd_format_xml,
cmd_get_http_authentication_summaries,
cmd_get_http_authentication_config,
cmd_get_sse_events,
@@ -1386,7 +1442,9 @@ pub fn run() {
cmd_save_response,
cmd_send_ephemeral_request,
cmd_send_http_request,
cmd_template_functions,
cmd_send_folder,
cmd_template_function_config,
cmd_template_function_summaries,
cmd_template_tokens_to_string,
//
//
@@ -1404,7 +1462,7 @@ pub fn run() {
let _ = window::create_main_window(app_handle, "/");
let h = app_handle.clone();
tauri::async_runtime::spawn(async move {
let info = history::store_launch_history(&h).await;
let info = history::get_or_upsert_launch_info(&h);
debug!("Launched Yaak {:?}", info);
});
@@ -1511,14 +1569,17 @@ fn monitor_plugin_events<R: Runtime>(app_handle: &AppHandle<R>) {
Ok(None) => return,
Err(e) => {
warn!("Failed to handle plugin event: {e:?}");
let _ = app_handle.emit("show_toast", InternalEventPayload::ShowToastRequest(ShowToastRequest {
message: e.to_string(),
color: Some(Color::Danger),
icon: None,
timeout: Some(30000),
}));
let _ = app_handle.emit(
"show_toast",
InternalEventPayload::ShowToastRequest(ShowToastRequest {
message: e.to_string(),
color: Some(Color::Danger),
icon: None,
timeout: Some(30000),
}),
);
return;
},
}
};
let plugin_manager: State<'_, PluginManager> = app_handle.state();

View File

@@ -1,7 +1,7 @@
use std::time::SystemTime;
use crate::error::Result;
use crate::history::get_num_launches;
use crate::history::get_or_upsert_launch_info;
use chrono::{DateTime, Utc};
use log::debug;
use reqwest::Method;
@@ -79,7 +79,7 @@ impl YaakNotifier {
#[cfg(feature = "license")]
let license_check = {
use yaak_license::{LicenseCheckStatus, check_license};
use yaak_license::{check_license, LicenseCheckStatus};
match check_license(window).await {
Ok(LicenseCheckStatus::PersonalUse { .. }) => "personal".to_string(),
Ok(LicenseCheckStatus::CommercialUse) => "commercial".to_string(),
@@ -91,17 +91,17 @@ impl YaakNotifier {
#[cfg(not(feature = "license"))]
let license_check = "disabled".to_string();
let settings = window.db().get_settings();
let num_launches = get_num_launches(app_handle).await;
let info = app_handle.package_info().clone();
let launch_info = get_or_upsert_launch_info(app_handle);
let req = yaak_api_client(app_handle)?
.request(Method::GET, "https://notify.yaak.app/notifications")
.query(&[
("version", info.version.to_string().as_str()),
("launches", num_launches.to_string().as_str()),
("installed", settings.created_at.format("%Y-%m-%d").to_string().as_str()),
("version", &launch_info.current_version),
("version_prev", &launch_info.previous_version),
("launches", &launch_info.num_launches.to_string()),
("installed", &launch_info.user_since.format("%Y-%m-%d").to_string()),
("license", &license_check),
("platform", get_os()),
("updates", &get_updater_status(app_handle).to_string()),
("platform", &get_os().to_string()),
]);
let resp = req.send().await?;
if resp.status() != 200 {
@@ -131,3 +131,32 @@ async fn get_kv<R: Runtime>(app_handle: &AppHandle<R>) -> Result<Vec<String>> {
Some(v) => Ok(serde_json::from_str(&v.value)?),
}
}
fn get_updater_status<R: Runtime>(app_handle: &AppHandle<R>) -> &'static str {
#[cfg(not(feature = "updater"))]
{
// Updater is not enabled as a Rust feature
return "missing";
}
#[cfg(all(feature = "updater", target_os = "linux"))]
{
let settings = app_handle.db().get_settings();
if !settings.autoupdate {
// Updates are explicitly disabled
"disabled"
} else if std::env::var("APPIMAGE").is_err() {
// Updates are enabled, but unsupported
"unsupported"
} else {
// Updates are enabled and supported
"enabled"
}
}
#[cfg(all(feature = "updater", not(target_os = "linux")))]
{
let settings = app_handle.db().get_settings();
if settings.autoupdate { "enabled" } else { "disabled" }
}
}

View File

@@ -1,3 +1,4 @@
use crate::error::Result;
use crate::window_menu::app_menu;
use log::{info, warn};
use rand::random;
@@ -6,7 +7,6 @@ use tauri::{
};
use tauri_plugin_opener::OpenerExt;
use tokio::sync::mpsc;
use crate::error::Result;
const DEFAULT_WINDOW_WIDTH: f64 = 1100.0;
const DEFAULT_WINDOW_HEIGHT: f64 = 600.0;
@@ -49,7 +49,6 @@ pub(crate) fn create_window<R: Runtime>(
.resizable(true)
.visible(false) // To prevent theme flashing, the frontend code calls show() immediately after configuring the theme
.fullscreen(false)
.disable_drag_drop_handler() // Required for frontend Dnd on windows
.min_inner_size(MIN_WINDOW_WIDTH, MIN_WINDOW_HEIGHT);
if let Some(key) = config.data_dir_key {
@@ -216,10 +215,10 @@ pub(crate) fn create_child_window(
) -> Result<WebviewWindow> {
let app_handle = parent_window.app_handle();
let label = format!("{OTHER_WINDOW_PREFIX}_{label}");
let scale_factor = parent_window.scale_factor().unwrap();
let scale_factor = parent_window.scale_factor()?;
let current_pos = parent_window.inner_position().unwrap().to_logical::<f64>(scale_factor);
let current_size = parent_window.inner_size().unwrap().to_logical::<f64>(scale_factor);
let current_pos = parent_window.inner_position()?.to_logical::<f64>(scale_factor);
let current_size = parent_window.inner_size()?.to_logical::<f64>(scale_factor);
// Position the new window in the middle of the parent
let position = (

View File

@@ -30,7 +30,7 @@
}
},
"bundle": {
"createUpdaterArtifacts": "v1Compatible",
"createUpdaterArtifacts": true,
"windows": {
"signCommand": "trusted-signing-cli -e https://eus.codesigning.azure.net/ -a Yaak -c yaakapp %1"
}

View File

@@ -88,7 +88,7 @@ pub async fn activate_license<R: Runtime>(
}
let body: ActivateLicenseResponsePayload = response.json().await?;
window.app_handle().db().set_key_value_string(
window.app_handle().db().set_key_value_str(
KV_ACTIVATION_ID_KEY,
KV_NAMESPACE,
body.activation_id.as_str(),
@@ -207,5 +207,5 @@ fn build_url(path: &str) -> String {
}
pub async fn get_activation_id<R: Runtime>(app_handle: &AppHandle<R>) -> String {
app_handle.db().get_key_value_string(KV_ACTIVATION_ID_KEY, KV_NAMESPACE, "")
app_handle.db().get_key_value_str(KV_ACTIVATION_ID_KEY, KV_NAMESPACE, "")
}

View File

@@ -22,8 +22,8 @@ sha2 = { workspace = true }
tauri = { workspace = true }
tauri-plugin-dialog = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true }
ts-rs = { workspace = true, features = ["chrono-impl", "serde-json-impl"] }
yaak-common = { workspace = true }
[build-dependencies]
tauri-plugin = { workspace = true, features = ["build"] }

View File

@@ -81,11 +81,12 @@ export function getAnyModel(id: string): AnyModel | null {
}
export function getModel<M extends AnyModel['model'], T extends ExtractModel<AnyModel, M>>(
modelType: M | M[],
modelType: M | ReadonlyArray<M>,
id: string,
): T | null {
let data = mustStore().get(modelStoreDataAtom);
for (const t of Array.isArray(modelType) ? modelType : [modelType]) {
const types: ReadonlyArray<M> = Array.isArray(modelType) ? modelType : [modelType];
for (const t of types) {
let v = data[t][id];
if (v?.model === t) return v as T;
}
@@ -139,7 +140,7 @@ export async function deleteModel<M extends AnyModel['model'], T extends Extract
export function duplicateModelById<
M extends AnyModel['model'],
T extends ExtractModel<AnyModel, M>,
>(modelType: M | M[], id: string) {
>(modelType: M | ReadonlyArray<M>, id: string) {
let model = getModel<M, T>(modelType, id);
return duplicateModel(model);
}
@@ -150,6 +151,8 @@ export function duplicateModel<M extends AnyModel['model'], T extends ExtractMod
if (model == null) {
throw new Error('Failed to delete null model');
}
if ('sortPriority' in model) model.sortPriority = model.sortPriority + 0.0001;
return invoke<string>('plugin:yaak-models|duplicate', { model });
}

View File

@@ -1,6 +1,6 @@
use crate::db_context::DbContext;
use crate::error::Result;
use crate::models::{HttpRequest, HttpRequestHeader, HttpRequestIden};
use crate::models::{Folder, FolderIden, HttpRequest, HttpRequestHeader, HttpRequestIden};
use crate::util::UpdateSource;
use serde_json::Value;
use std::collections::BTreeMap;
@@ -89,4 +89,18 @@ impl<'a> DbContext<'a> {
Ok(headers)
}
pub fn list_http_requests_for_folder_recursive(
&self,
folder_id: &str,
) -> Result<Vec<HttpRequest>> {
let mut children = Vec::new();
for m in self.find_many::<Folder>(FolderIden::FolderId, folder_id, None)? {
children.extend(self.list_http_requests_for_folder_recursive(&m.id)?);
}
for m in self.find_many::<HttpRequest>(FolderIden::FolderId, folder_id, None)? {
children.push(m);
}
Ok(children)
}
}

View File

@@ -1,3 +1,4 @@
use chrono::NaiveDateTime;
use crate::db_context::DbContext;
use crate::error::Result;
use crate::models::{KeyValue, KeyValueIden, UpsertModelInfo};
@@ -22,7 +23,7 @@ impl<'a> DbContext<'a> {
Ok(items.map(|v| v.unwrap()).collect())
}
pub fn get_key_value_string(&self, namespace: &str, key: &str, default: &str) -> String {
pub fn get_key_value_str(&self, namespace: &str, key: &str, default: &str) -> String {
match self.get_key_value_raw(namespace, key) {
None => default.to_string(),
Some(v) => {
@@ -38,6 +39,22 @@ impl<'a> DbContext<'a> {
}
}
pub fn get_key_value_dte(&self, namespace: &str, key: &str, default: NaiveDateTime) -> NaiveDateTime {
match self.get_key_value_raw(namespace, key) {
None => default,
Some(v) => {
let result = serde_json::from_str(&v.value);
match result {
Ok(v) => v,
Err(e) => {
error!("Failed to parse date key value: {}", e);
default
}
}
}
}
}
pub fn get_key_value_int(&self, namespace: &str, key: &str, default: i32) -> i32 {
match self.get_key_value_raw(namespace, key) {
None => default.clone(),
@@ -67,7 +84,18 @@ impl<'a> DbContext<'a> {
self.conn.resolve().query_row(sql.as_str(), &*params.as_params(), KeyValue::from_row).ok()
}
pub fn set_key_value_string(
pub fn set_key_value_dte(
&self,
namespace: &str,
key: &str,
value: NaiveDateTime,
source: &UpdateSource,
) -> (KeyValue, bool) {
let encoded = serde_json::to_string(&value).unwrap();
self.set_key_value_raw(namespace, key, &encoded, source)
}
pub fn set_key_value_str(
&self,
namespace: &str,
key: &str,

View File

@@ -3,6 +3,7 @@ use crate::models::{
AnyModel, Environment, Folder, GrpcRequest, HttpRequest, UpsertModelInfo, WebsocketRequest,
Workspace, WorkspaceIden,
};
use yaak_common::window::WorkspaceWindowTrait;
use crate::query_manager::QueryManagerExt;
use chrono::{NaiveDateTime, Utc};
use log::warn;
@@ -158,7 +159,17 @@ pub fn get_workspace_export_resources<R: Runtime>(
Ok(data)
}
pub fn maybe_gen_id<M: UpsertModelInfo>(id: &str, ids: &mut BTreeMap<String, String>) -> String {
pub fn maybe_gen_id<M: UpsertModelInfo, R: Runtime>(
window: &WebviewWindow<R>,
id: &str,
ids: &mut BTreeMap<String, String>,
) -> String {
if id == "CURRENT_WORKSPACE" {
if let Some(wid) = window.workspace_id() {
return wid.to_string();
}
}
if !id.starts_with("GENERATE_ID::") {
return id.to_string();
}
@@ -173,12 +184,13 @@ pub fn maybe_gen_id<M: UpsertModelInfo>(id: &str, ids: &mut BTreeMap<String, Str
}
}
pub fn maybe_gen_id_opt<M: UpsertModelInfo>(
pub fn maybe_gen_id_opt<M: UpsertModelInfo, R: Runtime>(
window: &WebviewWindow<R>,
id: Option<String>,
ids: &mut BTreeMap<String, String>,
) -> Option<String> {
match id {
Some(id) => Some(maybe_gen_id::<M>(id.as_str(), ids)),
Some(id) => Some(maybe_gen_id::<M, R>(window, id.as_str(), ids)),
None => None,
}
}

View File

@@ -361,7 +361,11 @@ export type GetKeyValueRequest = { key: string, };
export type GetKeyValueResponse = { value?: string, };
export type GetTemplateFunctionsResponse = { functions: Array<TemplateFunction>, pluginRefId: string, };
export type GetTemplateFunctionConfigRequest = { contextId: string, name: string, values: { [key in string]?: JsonPrimitive }, };
export type GetTemplateFunctionConfigResponse = { function: TemplateFunction, pluginRefId: string, };
export type GetTemplateFunctionSummaryResponse = { functions: Array<TemplateFunction>, pluginRefId: string, };
export type GetThemesRequest = Record<string, never>;
@@ -385,7 +389,7 @@ export type ImportResponse = { resources: ImportResources, };
export type InternalEvent = { id: string, pluginRefId: string, pluginName: string, replyId: string | null, windowContext: PluginWindowContext, payload: InternalEventPayload, };
export type InternalEventPayload = { "type": "boot_request" } & BootRequest | { "type": "boot_response" } | { "type": "reload_response" } & ReloadResponse | { "type": "terminate_request" } | { "type": "terminate_response" } | { "type": "import_request" } & ImportRequest | { "type": "import_response" } & ImportResponse | { "type": "filter_request" } & FilterRequest | { "type": "filter_response" } & FilterResponse | { "type": "export_http_request_request" } & ExportHttpRequestRequest | { "type": "export_http_request_response" } & ExportHttpRequestResponse | { "type": "send_http_request_request" } & SendHttpRequestRequest | { "type": "send_http_request_response" } & SendHttpRequestResponse | { "type": "list_cookie_names_request" } & ListCookieNamesRequest | { "type": "list_cookie_names_response" } & ListCookieNamesResponse | { "type": "get_cookie_value_request" } & GetCookieValueRequest | { "type": "get_cookie_value_response" } & GetCookieValueResponse | { "type": "get_http_request_actions_request" } & EmptyPayload | { "type": "get_http_request_actions_response" } & GetHttpRequestActionsResponse | { "type": "call_http_request_action_request" } & CallHttpRequestActionRequest | { "type": "get_grpc_request_actions_request" } & EmptyPayload | { "type": "get_grpc_request_actions_response" } & GetGrpcRequestActionsResponse | { "type": "call_grpc_request_action_request" } & CallGrpcRequestActionRequest | { "type": "get_template_functions_request" } | { "type": "get_template_functions_response" } & GetTemplateFunctionsResponse | { "type": "call_template_function_request" } & CallTemplateFunctionRequest | { "type": "call_template_function_response" } & CallTemplateFunctionResponse | { "type": "get_http_authentication_summary_request" } & EmptyPayload | { "type": "get_http_authentication_summary_response" } & GetHttpAuthenticationSummaryResponse | { "type": "get_http_authentication_config_request" } & GetHttpAuthenticationConfigRequest | { "type": "get_http_authentication_config_response" } & GetHttpAuthenticationConfigResponse | { "type": "call_http_authentication_request" } & CallHttpAuthenticationRequest | { "type": "call_http_authentication_response" } & CallHttpAuthenticationResponse | { "type": "call_http_authentication_action_request" } & CallHttpAuthenticationActionRequest | { "type": "call_http_authentication_action_response" } & EmptyPayload | { "type": "copy_text_request" } & CopyTextRequest | { "type": "copy_text_response" } & EmptyPayload | { "type": "render_http_request_request" } & RenderHttpRequestRequest | { "type": "render_http_request_response" } & RenderHttpRequestResponse | { "type": "render_grpc_request_request" } & RenderGrpcRequestRequest | { "type": "render_grpc_request_response" } & RenderGrpcRequestResponse | { "type": "get_key_value_request" } & GetKeyValueRequest | { "type": "get_key_value_response" } & GetKeyValueResponse | { "type": "set_key_value_request" } & SetKeyValueRequest | { "type": "set_key_value_response" } & SetKeyValueResponse | { "type": "delete_key_value_request" } & DeleteKeyValueRequest | { "type": "delete_key_value_response" } & DeleteKeyValueResponse | { "type": "open_window_request" } & OpenWindowRequest | { "type": "window_navigate_event" } & WindowNavigateEvent | { "type": "window_close_event" } | { "type": "close_window_request" } & CloseWindowRequest | { "type": "template_render_request" } & TemplateRenderRequest | { "type": "template_render_response" } & TemplateRenderResponse | { "type": "show_toast_request" } & ShowToastRequest | { "type": "show_toast_response" } & EmptyPayload | { "type": "prompt_text_request" } & PromptTextRequest | { "type": "prompt_text_response" } & PromptTextResponse | { "type": "get_http_request_by_id_request" } & GetHttpRequestByIdRequest | { "type": "get_http_request_by_id_response" } & GetHttpRequestByIdResponse | { "type": "find_http_responses_request" } & FindHttpResponsesRequest | { "type": "find_http_responses_response" } & FindHttpResponsesResponse | { "type": "get_themes_request" } & GetThemesRequest | { "type": "get_themes_response" } & GetThemesResponse | { "type": "empty_response" } & EmptyPayload | { "type": "error_response" } & ErrorResponse;
export type InternalEventPayload = { "type": "boot_request" } & BootRequest | { "type": "boot_response" } | { "type": "reload_response" } & ReloadResponse | { "type": "terminate_request" } | { "type": "terminate_response" } | { "type": "import_request" } & ImportRequest | { "type": "import_response" } & ImportResponse | { "type": "filter_request" } & FilterRequest | { "type": "filter_response" } & FilterResponse | { "type": "export_http_request_request" } & ExportHttpRequestRequest | { "type": "export_http_request_response" } & ExportHttpRequestResponse | { "type": "send_http_request_request" } & SendHttpRequestRequest | { "type": "send_http_request_response" } & SendHttpRequestResponse | { "type": "list_cookie_names_request" } & ListCookieNamesRequest | { "type": "list_cookie_names_response" } & ListCookieNamesResponse | { "type": "get_cookie_value_request" } & GetCookieValueRequest | { "type": "get_cookie_value_response" } & GetCookieValueResponse | { "type": "get_http_request_actions_request" } & EmptyPayload | { "type": "get_http_request_actions_response" } & GetHttpRequestActionsResponse | { "type": "call_http_request_action_request" } & CallHttpRequestActionRequest | { "type": "get_grpc_request_actions_request" } & EmptyPayload | { "type": "get_grpc_request_actions_response" } & GetGrpcRequestActionsResponse | { "type": "call_grpc_request_action_request" } & CallGrpcRequestActionRequest | { "type": "get_template_function_summary_request" } & EmptyPayload | { "type": "get_template_function_summary_response" } & GetTemplateFunctionSummaryResponse | { "type": "get_template_function_config_request" } & GetTemplateFunctionConfigRequest | { "type": "get_template_function_config_response" } & GetTemplateFunctionConfigResponse | { "type": "call_template_function_request" } & CallTemplateFunctionRequest | { "type": "call_template_function_response" } & CallTemplateFunctionResponse | { "type": "get_http_authentication_summary_request" } & EmptyPayload | { "type": "get_http_authentication_summary_response" } & GetHttpAuthenticationSummaryResponse | { "type": "get_http_authentication_config_request" } & GetHttpAuthenticationConfigRequest | { "type": "get_http_authentication_config_response" } & GetHttpAuthenticationConfigResponse | { "type": "call_http_authentication_request" } & CallHttpAuthenticationRequest | { "type": "call_http_authentication_response" } & CallHttpAuthenticationResponse | { "type": "call_http_authentication_action_request" } & CallHttpAuthenticationActionRequest | { "type": "call_http_authentication_action_response" } & EmptyPayload | { "type": "copy_text_request" } & CopyTextRequest | { "type": "copy_text_response" } & EmptyPayload | { "type": "render_http_request_request" } & RenderHttpRequestRequest | { "type": "render_http_request_response" } & RenderHttpRequestResponse | { "type": "render_grpc_request_request" } & RenderGrpcRequestRequest | { "type": "render_grpc_request_response" } & RenderGrpcRequestResponse | { "type": "template_render_request" } & TemplateRenderRequest | { "type": "template_render_response" } & TemplateRenderResponse | { "type": "get_key_value_request" } & GetKeyValueRequest | { "type": "get_key_value_response" } & GetKeyValueResponse | { "type": "set_key_value_request" } & SetKeyValueRequest | { "type": "set_key_value_response" } & SetKeyValueResponse | { "type": "delete_key_value_request" } & DeleteKeyValueRequest | { "type": "delete_key_value_response" } & DeleteKeyValueResponse | { "type": "open_window_request" } & OpenWindowRequest | { "type": "window_navigate_event" } & WindowNavigateEvent | { "type": "window_close_event" } | { "type": "close_window_request" } & CloseWindowRequest | { "type": "show_toast_request" } & ShowToastRequest | { "type": "show_toast_response" } & EmptyPayload | { "type": "prompt_text_request" } & PromptTextRequest | { "type": "prompt_text_response" } & PromptTextResponse | { "type": "get_http_request_by_id_request" } & GetHttpRequestByIdRequest | { "type": "get_http_request_by_id_response" } & GetHttpRequestByIdResponse | { "type": "find_http_responses_request" } & FindHttpResponsesRequest | { "type": "find_http_responses_response" } & FindHttpResponsesResponse | { "type": "get_themes_request" } & GetThemesRequest | { "type": "get_themes_response" } & GetThemesResponse | { "type": "empty_response" } & EmptyPayload | { "type": "error_response" } & ErrorResponse;
export type JsonPrimitive = string | number | boolean | null;

View File

@@ -6,7 +6,7 @@ use reqwest::{Response, Url};
use serde::{Deserialize, Serialize};
use std::path::Path;
use std::str::FromStr;
use tauri::{AppHandle, Runtime, is_dev};
use tauri::{AppHandle, Runtime};
use ts_rs::TS;
use yaak_common::api_client::yaak_api_client;
use yaak_models::query_manager::QueryManagerExt;
@@ -96,11 +96,7 @@ pub async fn search_plugins<R: Runtime>(
}
fn build_url(path: &str) -> Url {
let base_url = if is_dev() {
"http://localhost:9444/api/v1/plugins"
} else {
"https://api.yaak.app/api/v1/plugins"
};
let base_url = "https://api.yaak.app/api/v1/plugins";
Url::from_str(&format!("{base_url}{path}")).unwrap()
}

View File

@@ -99,8 +99,10 @@ pub enum InternalEventPayload {
CallGrpcRequestActionRequest(CallGrpcRequestActionRequest),
// Template Functions
GetTemplateFunctionsRequest,
GetTemplateFunctionsResponse(GetTemplateFunctionsResponse),
GetTemplateFunctionSummaryRequest(EmptyPayload),
GetTemplateFunctionSummaryResponse(GetTemplateFunctionSummaryResponse),
GetTemplateFunctionConfigRequest(GetTemplateFunctionConfigRequest),
GetTemplateFunctionConfigResponse(GetTemplateFunctionConfigResponse),
CallTemplateFunctionRequest(CallTemplateFunctionRequest),
CallTemplateFunctionResponse(CallTemplateFunctionResponse),
@@ -673,11 +675,28 @@ pub struct CallHttpAuthenticationResponse {
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_events.ts")]
pub struct GetTemplateFunctionsResponse {
pub struct GetTemplateFunctionSummaryResponse {
pub functions: Vec<TemplateFunction>,
pub plugin_ref_id: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_events.ts")]
pub struct GetTemplateFunctionConfigRequest {
pub context_id: String,
pub name: String,
pub values: HashMap<String, JsonPrimitive>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_events.ts")]
pub struct GetTemplateFunctionConfigResponse {
pub function: TemplateFunction,
pub plugin_ref_id: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_events.ts")]

View File

@@ -10,7 +10,8 @@ use crate::events::{
FilterRequest, FilterResponse, GetGrpcRequestActionsResponse,
GetHttpAuthenticationConfigRequest, GetHttpAuthenticationConfigResponse,
GetHttpAuthenticationSummaryResponse, GetHttpRequestActionsResponse,
GetTemplateFunctionsResponse, GetThemesRequest, GetThemesResponse, ImportRequest,
GetTemplateFunctionConfigRequest, GetTemplateFunctionConfigResponse,
GetTemplateFunctionSummaryResponse, GetThemesRequest, GetThemesResponse, ImportRequest,
ImportResponse, InternalEvent, InternalEventPayload, JsonPrimitive, PluginWindowContext,
RenderPurpose,
};
@@ -489,35 +490,71 @@ impl PluginManager {
Ok(all_actions)
}
pub async fn get_template_functions<R: Runtime>(
pub async fn get_template_function_config<R: Runtime>(
&self,
window: &WebviewWindow<R>,
) -> Result<Vec<GetTemplateFunctionsResponse>> {
self.get_template_functions_with_context(&PluginWindowContext::new(&window)).await
}
fn_name: &str,
environment_chain: Vec<Environment>,
values: HashMap<String, JsonPrimitive>,
model_id: &str,
) -> Result<GetTemplateFunctionConfigResponse> {
let results = self.get_template_function_summaries(window).await?;
let r = results
.iter()
.find(|r| r.functions.iter().any(|f| f.name == fn_name))
.ok_or_else(|| PluginNotFoundErr(fn_name.into()))?;
pub async fn get_template_functions_with_context(
&self,
window_context: &PluginWindowContext,
) -> Result<Vec<GetTemplateFunctionsResponse>> {
let reply_events = self
.send_and_wait(window_context, &InternalEventPayload::GetTemplateFunctionsRequest)
.await?;
let mut result = Vec::new();
for event in reply_events {
if let InternalEventPayload::GetTemplateFunctionsResponse(resp) = event.payload {
result.push(resp.clone());
let plugin = match self.get_plugin_by_ref_id(&r.plugin_ref_id).await {
None => {
// It's probably a native function, so just fallback to the summary
let function = r
.functions
.iter()
.find(|f| f.name == fn_name)
.ok_or_else(|| PluginNotFoundErr(fn_name.into()))?;
return Ok(GetTemplateFunctionConfigResponse {
function: function.clone(),
plugin_ref_id: r.plugin_ref_id.clone(),
});
}
Some(v) => v,
};
let window_context = &PluginWindowContext::new(&window);
let vars = &make_vars_hashmap(environment_chain);
let cb = PluginTemplateCallback::new(
window.app_handle(),
&window_context,
RenderPurpose::Preview,
);
// We don't want to fail for this op because the UI will not be able to list any auth types then
let render_opt = RenderOptions {
error_behavior: RenderErrorBehavior::ReturnEmpty,
};
let rendered_values = render_json_value_raw(json!(values), vars, &cb, &render_opt).await?;
let context_id = format!("{:x}", md5::compute(model_id.to_string()));
let event = self
.send_to_plugin_and_wait(
&PluginWindowContext::new(window),
&plugin,
&InternalEventPayload::GetTemplateFunctionConfigRequest(
GetTemplateFunctionConfigRequest {
values: serde_json::from_value(rendered_values)?,
name: fn_name.to_string(),
context_id,
},
),
)
.await?;
match event.payload {
InternalEventPayload::GetTemplateFunctionConfigResponse(resp) => Ok(resp),
InternalEventPayload::EmptyResponse(_) => {
Err(PluginErr("Template function plugin returned empty".to_string()))
}
InternalEventPayload::ErrorResponse(e) => Err(PluginErr(e.error)),
e => Err(PluginErr(format!("Template function plugin returned invalid event {:?}", e))),
}
// Add Rust-based functions
result.push(GetTemplateFunctionsResponse {
plugin_ref_id: "__NATIVE__".to_string(), // Meh
functions: vec![template_function_secure(), template_function_keyring()],
});
Ok(result)
}
pub async fn call_http_request_action<R: Runtime>(
@@ -587,7 +624,7 @@ impl PluginManager {
environment_chain: Vec<Environment>,
auth_name: &str,
values: HashMap<String, JsonPrimitive>,
request_id: &str,
model_id: &str,
) -> Result<GetHttpAuthenticationConfigResponse> {
let results = self.get_http_authentication_summaries(window).await?;
let plugin = results
@@ -606,7 +643,7 @@ impl PluginManager {
error_behavior: RenderErrorBehavior::ReturnEmpty,
};
let rendered_values = render_json_value_raw(json!(values), vars, &cb, &render_opt).await?;
let context_id = format!("{:x}", md5::compute(request_id.to_string()));
let context_id = format!("{:x}", md5::compute(model_id.to_string()));
let event = self
.send_to_plugin_and_wait(
&PluginWindowContext::new(window),
@@ -720,6 +757,34 @@ impl PluginManager {
}
}
pub async fn get_template_function_summaries<R: Runtime>(
&self,
window: &WebviewWindow<R>,
) -> Result<Vec<GetTemplateFunctionSummaryResponse>> {
let window_context = PluginWindowContext::new(window);
let reply_events = self
.send_and_wait(
&window_context,
&InternalEventPayload::GetTemplateFunctionSummaryRequest(EmptyPayload {}),
)
.await?;
let mut results = Vec::new();
for event in reply_events {
if let InternalEventPayload::GetTemplateFunctionSummaryResponse(resp) = event.payload {
results.push(resp.clone());
}
}
// Add Rust-based functions
results.push(GetTemplateFunctionSummaryResponse {
plugin_ref_id: "__NATIVE__".to_string(), // Meh
functions: vec![template_function_secure(), template_function_keyring()],
});
Ok(results)
}
pub async fn call_template_function(
&self,
window_context: &PluginWindowContext,

View File

@@ -78,6 +78,10 @@ pub fn template_function_secure_run<R: Runtime>(
_ => return Ok("".to_string()),
};
if value.is_empty() {
return Ok("".to_string());
}
let value = match value.strip_prefix("YENC_") {
None => {
return Err(RenderError("Could not decrypt non-encrypted value".to_string()));

View File

@@ -1,7 +1,15 @@
export * from './bindings/parser';
import { Tokens } from './bindings/parser';
import { parse_template } from './pkg';
import { escape_template, parse_template, unescape_template } from './pkg';
export function parseTemplate(template: string) {
return parse_template(template) as Tokens;
}
export function escapeTemplate(template: string) {
return escape_template(template) as string;
}
export function unescapeTemplate(template: string) {
return unescape_template(template) as string;
}

View File

@@ -0,0 +1,166 @@
pub fn escape_template(text: &str) -> String {
let mut result = String::with_capacity(text.len());
let chars: Vec<char> = text.chars().collect();
let mut i = 0;
while i < chars.len() {
// Check if we're at "${["
if i + 2 < chars.len() && chars[i] == '$' && chars[i + 1] == '{' && chars[i + 2] == '[' {
// Count preceding backslashes
let mut backslash_count = 0;
let mut j = i;
while j > 0 && chars[j - 1] == '\\' {
backslash_count += 1;
j -= 1;
}
// If odd number of backslashes, the $ is escaped
// If even number (including 0), the $ is not escaped
let already_escaped = backslash_count % 2 == 1;
if already_escaped {
// Already escaped, just add the current character
result.push(chars[i]);
} else {
// Not escaped, add backslash before $
result.push('\\');
result.push(chars[i]);
}
} else {
result.push(chars[i]);
}
i += 1;
}
result
}
pub fn unescape_template(text: &str) -> String {
let mut result = String::with_capacity(text.len());
let chars: Vec<char> = text.chars().collect();
let mut i = 0;
while i < chars.len() {
// Check if we're at "\${["
if i + 3 < chars.len()
&& chars[i] == '\\'
&& chars[i + 1] == '$'
&& chars[i + 2] == '{'
&& chars[i + 3] == '['
{
// Count preceding backslashes (before the current backslash)
let mut backslash_count = 0;
let mut j = i;
while j > 0 && chars[j - 1] == '\\' {
backslash_count += 1;
j -= 1;
}
// If even number of preceding backslashes, this backslash escapes the $
// If odd number, this backslash is itself escaped
let escapes_dollar = backslash_count % 2 == 0;
if escapes_dollar {
// Skip the backslash, just add the $
result.push(chars[i + 1]);
i += 1; // Skip the backslash
} else {
// This backslash is escaped itself, keep it
result.push(chars[i]);
}
} else {
result.push(chars[i]);
}
i += 1;
}
result
}
#[cfg(test)]
mod tests {
use crate::escape::{escape_template, unescape_template};
#[test]
fn test_escape_simple() {
let input = r#"${[foo]}"#;
let expected = r#"\${[foo]}"#;
assert_eq!(escape_template(input), expected);
}
#[test]
fn test_already_escaped() {
let input = r#"\${[bar]}"#;
let expected = r#"\${[bar]}"#;
assert_eq!(escape_template(input), expected);
}
#[test]
fn test_double_backslash() {
let input = r#"\\${[bar]}"#;
let expected = r#"\\\${[bar]}"#;
assert_eq!(escape_template(input), expected);
}
#[test]
fn test_escape_with_surrounding_text() {
let input = r#"text ${[var]} more"#;
let expected = r#"text \${[var]} more"#;
assert_eq!(escape_template(input), expected);
}
#[test]
fn test_preserve_already_escaped() {
let input = r#"already \${[escaped]}"#;
let expected = r#"already \${[escaped]}"#;
assert_eq!(escape_template(input), expected);
}
#[test]
fn test_multiple_occurrences() {
let input = r#"${[one]} and ${[two]}"#;
let expected = r#"\${[one]} and \${[two]}"#;
assert_eq!(escape_template(input), expected);
}
#[test]
fn test_mixed_escaped_and_unescaped() {
let input = r#"mixed \${[esc]} and ${[unesc]}"#;
let expected = r#"mixed \${[esc]} and \${[unesc]}"#;
assert_eq!(escape_template(input), expected);
}
#[test]
fn test_unescape_simple() {
let input = r#"\${[foo]}"#;
let expected = r#"${[foo]}"#;
assert_eq!(unescape_template(input), expected);
}
#[test]
fn test_unescape_with_text() {
let input = r#"text \${[var]} more"#;
let expected = r#"text ${[var]} more"#;
assert_eq!(unescape_template(input), expected);
}
#[test]
fn test_unescape_multiple() {
let input = r#"\${[one]} and \${[two]}"#;
let expected = r#"${[one]} and ${[two]}"#;
assert_eq!(unescape_template(input), expected);
}
#[test]
fn test_unescape_double_backslash() {
let input = r#"\\\${[bar]}"#;
let expected = r#"\\${[bar]}"#;
assert_eq!(unescape_template(input), expected);
}
#[test]
fn test_unescape_plain_text() {
let input = r#"${[foo]}"#;
let expected = r#"${[foo]}"#;
assert_eq!(unescape_template(input), expected);
}
}

View File

@@ -0,0 +1,345 @@
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum XmlTok<'a> {
OpenTag { raw: &'a str, name: &'a str }, // "<tag ...>"
CloseTag { raw: &'a str, name: &'a str }, // "</tag>"
SelfCloseTag(&'a str), // "<tag .../>"
Comment(&'a str), // "<!-- ... -->"
CData(&'a str), // "<![CDATA[ ... ]]>"
ProcInst(&'a str), // "<?xml ...?>"
Doctype(&'a str), // "<!DOCTYPE ...>"
Text(&'a str), // "text between tags"
Template(&'a str), // "${[ ... ]}"
}
fn writeln_indented(out: &mut String, depth: usize, indent: &str, s: &str) {
for _ in 0..depth {
out.push_str(indent);
}
out.push_str(s);
out.push('\n');
}
pub fn format_xml(input: &str, indent: &str) -> String {
use XmlTok::*;
let tokens = tokenize_with_templates(input);
let mut out = String::new();
let mut depth = 0usize;
let mut i = 0usize;
while i < tokens.len() {
match tokens[i] {
OpenTag {
raw: open_raw,
name: open_name,
} => {
if i + 2 < tokens.len() {
if let Text(text_raw) = tokens[i + 1] {
let trimmed = text_raw.trim();
let no_newlines = !trimmed.contains('\n');
if no_newlines && !trimmed.is_empty() {
if let CloseTag {
raw: close_raw,
name: close_name,
} = tokens[i + 2]
{
if open_name == close_name {
for _ in 0..depth {
out.push_str(indent);
}
out.push_str(open_raw);
out.push_str(trimmed);
out.push_str(close_raw);
out.push('\n');
i += 3;
continue;
}
}
}
}
}
writeln_indented(&mut out, depth, indent, open_raw);
depth = depth.saturating_add(1);
i += 1;
}
CloseTag { raw, .. } => {
depth = depth.saturating_sub(1);
writeln_indented(&mut out, depth, indent, raw);
i += 1;
}
SelfCloseTag(raw) | Comment(raw) | ProcInst(raw) | Doctype(raw) | CData(raw)
| Template(raw) => {
writeln_indented(&mut out, depth, indent, raw);
i += 1;
}
Text(text_raw) => {
if text_raw.chars().any(|c| !c.is_whitespace()) {
let trimmed = text_raw.trim();
writeln_indented(&mut out, depth, indent, trimmed);
}
i += 1;
}
}
}
if out.ends_with('\n') {
out.pop();
}
out
}
fn tokenize_with_templates(input: &str) -> Vec<XmlTok<'_>> {
use XmlTok::*;
let bytes = input.as_bytes();
let mut i = 0usize;
let mut toks = Vec::<XmlTok>::new();
let starts_with =
|s: &[u8], i: usize, pat: &str| s.get(i..).map_or(false, |t| t.starts_with(pat.as_bytes()));
while i < bytes.len() {
// Template block: ${[ ... ]}
if starts_with(bytes, i, "${[") {
let start = i;
i += 3;
while i < bytes.len() && !starts_with(bytes, i, "]}") {
i += 1;
}
if starts_with(bytes, i, "]}") {
i += 2;
}
toks.push(Template(&input[start..i]));
continue;
}
if bytes[i] == b'<' {
// Comments
if starts_with(bytes, i, "<!--") {
let start = i;
i += 4;
while i < bytes.len() && !starts_with(bytes, i, "-->") {
i += 1;
}
if starts_with(bytes, i, "-->") {
i += 3;
}
toks.push(Comment(&input[start..i]));
continue;
}
// CDATA
if starts_with(bytes, i, "<![CDATA[") {
let start = i;
i += 9;
while i < bytes.len() && !starts_with(bytes, i, "]]>") {
i += 1;
}
if starts_with(bytes, i, "]]>") {
i += 3;
}
toks.push(CData(&input[start..i]));
continue;
}
// Processing Instruction
if starts_with(bytes, i, "<?") {
let start = i;
i += 2;
while i < bytes.len() && !starts_with(bytes, i, "?>") {
i += 1;
}
if starts_with(bytes, i, "?>") {
i += 2;
}
toks.push(ProcInst(&input[start..i]));
continue;
}
// DOCTYPE or other "<!"
if starts_with(bytes, i, "<!") {
let start = i;
i += 2;
while i < bytes.len() && bytes[i] != b'>' {
i += 1;
}
if i < bytes.len() {
i += 1;
}
toks.push(Doctype(&input[start..i]));
continue;
}
// Normal tag (open/close/self)
let start = i;
i += 1; // '<'
let is_close = if i < bytes.len() && bytes[i] == b'/' {
i += 1;
true
} else {
false
};
// read until '>' (respecting quotes)
let mut in_quote: Option<u8> = None;
while i < bytes.len() {
let c = bytes[i];
if let Some(q) = in_quote {
if c == q {
in_quote = None;
}
i += 1;
} else {
if c == b'\'' || c == b'"' {
in_quote = Some(c);
i += 1;
} else if c == b'>' {
i += 1;
break;
} else {
i += 1;
}
}
}
let raw = &input[start..i];
let is_self = raw.as_bytes().len() >= 2 && raw.as_bytes()[raw.len() - 2] == b'/';
if is_close {
let name = parse_close_name(raw);
toks.push(CloseTag { raw, name });
} else if is_self {
toks.push(SelfCloseTag(raw));
} else {
let name = parse_open_name(raw);
toks.push(OpenTag { raw, name });
}
continue;
}
// Text node until next '<' or template start
let start = i;
while i < bytes.len() && bytes[i] != b'<' && !starts_with(bytes, i, "${[") {
i += 1;
}
toks.push(XmlTok::Text(&input[start..i]));
}
toks
}
fn parse_open_name(raw: &str) -> &str {
// raw looks like "<name ...>" or "<name>"
// slice between '<' and first whitespace or '>' or '/>'
let s = &raw[1..]; // skip '<'
let end = s.find(|c: char| c.is_whitespace() || c == '>' || c == '/').unwrap_or(s.len());
&s[..end]
}
fn parse_close_name(raw: &str) -> &str {
// raw looks like "</name>"
let s = &raw[2..]; // skip "</"
let end = s.find('>').unwrap_or(s.len());
&s[..end]
}
#[cfg(test)]
mod tests {
use super::format_xml;
#[test]
fn inline_text_child() {
let src = r#"<root><foo>this might be a string</foo><bar attr="x">ok</bar></root>"#;
let want = r#"<root>
<foo>this might be a string</foo>
<bar attr="x">ok</bar>
</root>"#;
assert_eq!(format_xml(src, " "), want);
}
#[test]
fn works_when_nested() {
let src = r#"<root><foo><b>bold</b></foo></root>"#;
let want = r#"<root>
<foo>
<b>bold</b>
</foo>
</root>"#;
assert_eq!(format_xml(src, " "), want);
}
#[test]
fn trims_and_keeps_nonempty() {
let src = "<root><foo> hi </foo></root>";
let want = "<root>\n <foo>hi</foo>\n</root>";
assert_eq!(format_xml(src, " "), want);
}
#[test]
fn attributes_inline_text_child() {
// Keeps attributes verbatim and inlines simple text children
let src = r#"<root><item id="42" class='a b'>value</item></root>"#;
let want = r#"<root>
<item id="42" class='a b'>value</item>
</root>"#;
assert_eq!(format_xml(src, " "), want);
}
#[test]
fn attributes_with_irregular_spacing_preserved() {
// We don't normalize spaces inside the tag; raw is preserved
let src = r#"<root><a x = "1" y='2' >t</a></root>"#;
let want = r#"<root>
<a x = "1" y='2' >t</a>
</root>"#;
assert_eq!(format_xml(src, " "), want);
}
#[test]
fn self_closing_with_attributes() {
let src =
r#"<root><img src="x" alt='hello &quot;world&quot;' width="10" height="20"/></root>"#;
let want = r#"<root>
<img src="x" alt='hello &quot;world&quot;' width="10" height="20"/>
</root>"#;
assert_eq!(format_xml(src, " "), want);
}
#[test]
fn template_in_attribute_self_closing() {
let src = r#"<root><x attr=${[ compute(1, "two") ]}/></root>"#;
let want = r#"<root>
<x attr=${[ compute(1, "two") ]}/>
</root>"#;
assert_eq!(format_xml(src, " "), want);
}
#[test]
fn attributes_and_nested_children_expand() {
// Not inlined because child is an element, not plain text
let src = r#"<root><box kind="card"><b>bold</b></box></root>"#;
let want = r#"<root>
<box kind="card">
<b>bold</b>
</box>
</root>"#;
assert_eq!(format_xml(src, " "), want);
}
#[test]
fn namespace_and_xml_attrs() {
let src = r#"<root><ns:el xml:lang="en">ok</ns:el></root>"#;
let want = r#"<root>
<ns:el xml:lang="en">ok</ns:el>
</root>"#;
assert_eq!(format_xml(src, " "), want);
}
#[test]
fn mixed_quote_styles_in_attributes() {
// Single-quoted attr containing double quotes is fine; we don't re-quote
let src = r#"<root><a title='He said "hi"'>hello</a></root>"#;
let want = r#"<root>
<a title='He said "hi"'>hello</a>
</root>"#;
assert_eq!(format_xml(src, " "), want);
}
}

View File

@@ -1,8 +1,10 @@
pub mod error;
pub mod escape;
pub mod format;
pub mod parser;
pub mod renderer;
pub mod error;
pub mod wasm;
pub mod format_xml;
pub use parser::*;
pub use renderer::*;

View File

@@ -170,7 +170,13 @@ impl Parser {
let start_pos = self.pos;
while self.pos < self.chars.len() {
if self.match_str("${[") {
if self.match_str(r#"\\"#) {
// Skip double-escapes so we don't trigger our own escapes in the next case
self.curr_text += r#"\\"#;
} else if self.match_str(r#"\${["#) {
// Unescaped template syntax so we treat it as a string
self.curr_text += "${[";
} else if self.match_str("${[") {
let start_curr = self.pos;
if let Some(t) = self.parse_tag()? {
self.push_token(t);
@@ -490,6 +496,39 @@ mod tests {
use crate::error::Result;
use crate::*;
#[test]
fn escaped() -> Result<()> {
let mut p = Parser::new(r#"\${[ foo ]}"#);
assert_eq!(
p.parse()?.tokens,
vec![
Token::Raw {
text: "${[ foo ]}".to_string()
},
Token::Eof
]
);
Ok(())
}
#[test]
fn escaped_tricky() -> Result<()> {
let mut p = Parser::new(r#"\\${[ foo ]}"#);
assert_eq!(
p.parse()?.tokens,
vec![
Token::Raw {
text: r#"\\"#.to_string()
},
Token::Tag {
val: Val::Var { name: "foo".into() }
},
Token::Eof
]
);
Ok(())
}
#[test]
fn var_simple() -> Result<()> {
let mut p = Parser::new("${[ foo ]}");

View File

@@ -1,5 +1,5 @@
use crate::error::Result;
use crate::Parser;
use crate::{escape, Parser};
use wasm_bindgen::prelude::wasm_bindgen;
use wasm_bindgen::JsValue;
@@ -7,4 +7,16 @@ use wasm_bindgen::JsValue;
pub fn parse_template(template: &str) -> Result<JsValue> {
let tokens = Parser::new(template).parse()?;
Ok(serde_wasm_bindgen::to_value(&tokens).unwrap())
}
}
#[wasm_bindgen]
pub fn escape_template(template: &str) -> Result<JsValue> {
let escaped = escape::escape_template(template);
Ok(serde_wasm_bindgen::to_value(&escaped).unwrap())
}
#[wasm_bindgen]
pub fn unescape_template(template: &str) -> Result<JsValue> {
let escaped = escape::unescape_template(template);
Ok(serde_wasm_bindgen::to_value(&escaped).unwrap())
}

View File

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

View File

@@ -1,11 +1,21 @@
import { getModel } from '@yaakapp-internal/models';
import { Icon } from '../components/core/Icon';
import { HStack } from '../components/core/Stacks';
import type { FolderSettingsTab } from '../components/FolderSettingsDialog';
import { FolderSettingsDialog } from '../components/FolderSettingsDialog';
import { showDialog } from '../lib/dialog';
import { resolvedModelName } from '../lib/resolvedModelName';
export function openFolderSettings(folderId: string, tab?: FolderSettingsTab) {
const folder = getModel('folder', folderId);
showDialog({
id: 'folder-settings',
title: 'Folder Settings',
title: (
<HStack space={2} alignItems="center">
<Icon icon="folder_cog" size="xl" color="secondary" />
{resolvedModelName(folder)}
</HStack>
),
size: 'lg',
className: 'h-[50rem]',
noPadding: true,

View File

@@ -16,8 +16,8 @@ import { useAllRequests } from '../hooks/useAllRequests';
import { useCreateWorkspace } from '../hooks/useCreateWorkspace';
import { useDebouncedState } from '../hooks/useDebouncedState';
import { useEnvironmentsBreakdown } from '../hooks/useEnvironmentsBreakdown';
import { useGrpcRequestActions } from '../hooks/useGrpcRequestActions';
import type { HotkeyAction } from '../hooks/useHotKey';
import { useHotKey } from '../hooks/useHotKey';
import { useHttpRequestActions } from '../hooks/useHttpRequestActions';
import { useRecentEnvironments } from '../hooks/useRecentEnvironments';
import { useRecentRequests } from '../hooks/useRecentRequests';
@@ -61,6 +61,7 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
const [selectedItemKey, setSelectedItemKey] = useState<string | null>(null);
const activeEnvironment = useActiveEnvironment();
const httpRequestActions = useHttpRequestActions();
const grpcRequestActions = useGrpcRequestActions();
const workspaceId = useAtomValue(activeWorkspaceIdAtom);
const workspaces = useAtomValue(workspacesAtom);
const { baseEnvironment, subEnvironments } = useEnvironmentsBreakdown();
@@ -84,13 +85,18 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
action: 'settings.show',
onSelect: () => openSettings.mutate(null),
},
{
key: 'folder.create',
label: 'Create Folder',
onSelect: () => createFolder.mutate({}),
},
{
key: 'app.create',
label: 'Create Workspace',
onSelect: createWorkspace,
},
{
key: 'http_request.create',
key: 'model.create',
label: 'Create HTTP Request',
onSelect: () => createRequestAndNavigate({ model: 'http_request', workspaceId }),
},
@@ -142,8 +148,8 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
if (activeRequest?.model === 'http_request') {
commands.push({
key: 'http_request.send',
action: 'http_request.send',
key: 'request.send',
action: 'request.send',
label: 'Send Request',
onSelect: () => sendRequest(activeRequest.id),
});
@@ -157,6 +163,17 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
}
}
if (activeRequest?.model === 'grpc_request') {
for (let i = 0; i < grpcRequestActions.length; i++) {
const a = grpcRequestActions[i]!;
commands.push({
key: `grpc_request_action.${i}`,
label: a.label,
onSelect: () => a.call(activeRequest),
});
}
}
if (activeRequest != null) {
commands.push({
key: 'http_request.rename',
@@ -165,7 +182,7 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
});
commands.push({
key: 'sidebar.delete_selected_item',
key: 'sidebar.selected.delete',
label: 'Delete Request',
onSelect: () => deleteModelWithConfirm(activeRequest),
});
@@ -182,6 +199,7 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
activeRequest,
baseEnvironment,
createWorkspace,
grpcRequestActions,
httpRequestActions,
sendRequest,
setSidebarHidden,
@@ -369,7 +387,6 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
const handleKeyDown = useCallback(
(e: KeyboardEvent<HTMLInputElement>) => {
const index = filteredAllItems.findIndex((v) => v.key === selectedItem?.key);
if (e.key === 'ArrowDown' || (e.ctrlKey && e.key === 'n')) {
const next = filteredAllItems[index + 1] ?? filteredAllItems[0];
setSelectedItemKey(next?.key ?? null);
@@ -417,9 +434,7 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
active={v.key === selectedItem?.key}
key={v.key}
onClick={() => handleSelectAndClose(v.onSelect)}
rightSlot={
v.action && <CommandPaletteAction action={v.action} onAction={v.onSelect} />
}
rightSlot={v.action && <CommandPaletteAction action={v.action} />}
>
{v.label}
</CommandPaletteItem>
@@ -465,13 +480,6 @@ function CommandPaletteItem({
);
}
function CommandPaletteAction({
action,
onAction,
}: {
action: HotkeyAction;
onAction: () => void;
}) {
useHotKey(action, onAction);
function CommandPaletteAction({ action }: { action: HotkeyAction }) {
return <HotKey className="ml-auto" action={action} />;
}

View File

@@ -1,14 +1,17 @@
import classNames from 'classnames';
import type { CSSProperties} from 'react';
import React, { memo } from 'react';
interface Props {
className?: string;
style?: CSSProperties;
}
export const DropMarker = memo(
function DropMarker({ className }: Props) {
function DropMarker({ className, style }: Props) {
return (
<div
style={style}
className={classNames(
className,
'relative w-full h-0 overflow-visible pointer-events-none',

View File

@@ -21,7 +21,7 @@ import { resolvedModelName } from '../lib/resolvedModelName';
import { Banner } from './core/Banner';
import { Checkbox } from './core/Checkbox';
import { DetailsBanner } from './core/DetailsBanner';
import { Editor } from './core/Editor/Editor';
import { Editor } from './core/Editor/LazyEditor';
import { IconButton } from './core/IconButton';
import { Input } from './core/Input';
import { Label } from './core/Label';

View File

@@ -1,6 +1,7 @@
import type { Environment } from '@yaakapp-internal/models';
import { patchModel } from '@yaakapp-internal/models';
import type { GenericCompletionOption } from '@yaakapp-internal/plugins';
import classNames from 'classnames';
import React, { useCallback, useMemo } from 'react';
import { useEnvironmentsBreakdown } from '../hooks/useEnvironmentsBreakdown';
import { useIsEncryptionEnabled } from '../hooks/useIsEncryptionEnabled';
@@ -19,7 +20,6 @@ import { Heading } from './core/Heading';
import type { PairWithId } from './core/PairEditor';
import { ensurePairId } from './core/PairEditor.util';
import { PairOrBulkEditor } from './core/PairOrBulkEditor';
import { VStack } from './core/Stacks';
import { EnvironmentColorIndicator } from './EnvironmentColorIndicator';
import { EnvironmentSharableTooltip } from './EnvironmentSharableTooltip';
@@ -98,68 +98,69 @@ export function EnvironmentEditor({
};
return (
<VStack space={4} className={className}>
<Heading className="w-full flex items-center gap-0.5">
<EnvironmentColorIndicator clickToEdit environment={environment ?? null} />
{!hideName && <div className="mr-2">{environment?.name}</div>}
{isEncryptionEnabled ? (
!allVariableAreEncrypted ? (
<BadgeButton color="notice" onClick={() => encryptEnvironment(environment)}>
Encrypt All Variables
</BadgeButton>
<div className={classNames(className, 'h-full grid grid-rows-[auto_minmax(0,1fr)] gap-2')}>
<div className="flex flex-col gap-4">
<Heading className="w-full flex items-center gap-0.5">
<EnvironmentColorIndicator clickToEdit environment={environment ?? null} />
{!hideName && <div className="mr-2">{environment?.name}</div>}
{isEncryptionEnabled ? (
!allVariableAreEncrypted ? (
<BadgeButton color="notice" onClick={() => encryptEnvironment(environment)}>
Encrypt All Variables
</BadgeButton>
) : (
<BadgeButton color="secondary" onClick={setupOrConfigureEncryption}>
Encryption Settings
</BadgeButton>
)
) : (
<BadgeButton color="secondary" onClick={setupOrConfigureEncryption}>
Encryption Settings
<BadgeButton color="secondary" onClick={() => valueVisibility.set((v) => !v)}>
{valueVisibility.value ? 'Hide Values' : 'Show Values'}
</BadgeButton>
)
) : (
<BadgeButton color="secondary" onClick={() => valueVisibility.set((v) => !v)}>
{valueVisibility.value ? 'Hide Values' : 'Show Values'}
)}
<BadgeButton
color="secondary"
rightSlot={<EnvironmentSharableTooltip />}
onClick={async () => {
await patchModel(environment, { public: !environment.public });
}}
>
{environment.public ? 'Sharable' : 'Private'}
</BadgeButton>
</Heading>
{environment.public && (!isEncryptionEnabled || !allVariableAreEncrypted) && (
<DismissibleBanner
id={`warn-unencrypted-${environment.id}`}
color="notice"
className="mr-3"
actions={[
{
label: 'Encrypt Variables',
onClick: () => encryptEnvironment(environment),
color: 'success',
},
]}
>
This sharable environment contains plain-text secrets
</DismissibleBanner>
)}
<BadgeButton
color="secondary"
rightSlot={<EnvironmentSharableTooltip />}
onClick={async () => {
await patchModel(environment, { public: !environment.public });
}}
>
{environment.public ? 'Sharable' : 'Private'}
</BadgeButton>
</Heading>
{environment.public && (!isEncryptionEnabled || !allVariableAreEncrypted) && (
<DismissibleBanner
id={`warn-unencrypted-${environment.id}`}
color="notice"
className="mr-3"
actions={[
{
label: 'Encrypt Variables',
onClick: () => encryptEnvironment(environment),
color: 'success',
},
]}
>
This sharable environment contains plain-text secrets
</DismissibleBanner>
)}
<div className="h-full pr-2 pb-2 grid grid-rows-[minmax(0,1fr)] overflow-auto">
<PairOrBulkEditor
allowMultilineValues
preferenceName="environment"
nameAutocomplete={nameAutocomplete}
namePlaceholder="VAR_NAME"
nameValidate={validateName}
valueType={valueType}
valueAutocompleteVariables='environment'
valueAutocompleteFunctions
forceUpdateKey={`${environment.id}::${forceUpdateKey}`}
pairs={environment.variables}
onChange={handleChange}
stateKey={`environment.${environment.id}`}
forcedEnvironmentId={environment.id}
/>
</div>
</VStack>
<PairOrBulkEditor
className="h-full"
allowMultilineValues
preferenceName="environment"
nameAutocomplete={nameAutocomplete}
namePlaceholder="VAR_NAME"
nameValidate={validateName}
valueType={valueType}
valueAutocompleteVariables="environment"
valueAutocompleteFunctions
forceUpdateKey={`${environment.id}::${forceUpdateKey}`}
pairs={environment.variables}
onChange={handleChange}
stateKey={`environment.${environment.id}`}
forcedEnvironmentId={environment.id}
/>
</div>
);
}

View File

@@ -33,7 +33,7 @@ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundarySt
render() {
if (this.state.hasError) {
return (
<Banner color="danger" className="flex items-center gap-2">
<Banner color="danger" className="flex items-center gap-2 overflow-auto">
<div>
Error rendering <InlineCode>{this.props.name}</InlineCode> component
</div>

View File

@@ -0,0 +1,192 @@
import type { Folder, GrpcRequest, HttpRequest, WebsocketRequest } from '@yaakapp-internal/models';
import { foldersAtom } from '@yaakapp-internal/models';
import classNames from 'classnames';
import { useAtomValue } from 'jotai';
import type { CSSProperties } from 'react';
import { useCallback, useMemo } from 'react';
import { allRequestsAtom } from '../hooks/useAllRequests';
import { useLatestHttpResponse } from '../hooks/useLatestHttpResponse';
import { sendAnyHttpRequest } from '../hooks/useSendAnyHttpRequest';
import { showDialog } from '../lib/dialog';
import { resolvedModelName } from '../lib/resolvedModelName';
import { router } from '../lib/router';
import { Button } from './core/Button';
import { Heading } from './core/Heading';
import { HttpResponseDurationTag } from './core/HttpResponseDurationTag';
import { HttpStatusTag } from './core/HttpStatusTag';
import { Icon } from './core/Icon';
import { IconButton } from './core/IconButton';
import { LoadingIcon } from './core/LoadingIcon';
import { Separator } from './core/Separator';
import { SizeTag } from './core/SizeTag';
import { HStack } from './core/Stacks';
import { HttpResponsePane } from './HttpResponsePane';
interface Props {
folder: Folder;
style: CSSProperties;
}
export function FolderLayout({ folder, style }: Props) {
const folders = useAtomValue(foldersAtom);
const requests = useAtomValue(allRequestsAtom);
const children = useMemo(() => {
return [
...folders.filter((f) => f.folderId === folder.id),
...requests.filter((r) => r.folderId === folder.id),
];
}, [folder.id, folders, requests]);
return (
<div style={style} className="p-6 pt-4 overflow-y-auto @container">
<HStack space={2} alignItems="center">
<Icon icon="folder" size="xl" color="secondary" />
<Heading level={1}>{resolvedModelName(folder)}</Heading>
<HStack className="ml-auto" alignItems="center">
<Button rightSlot={<Icon icon="send_horizontal" />} color="secondary" size="sm" variant="border">Send All</Button>
</HStack>
</HStack>
<Separator className="mt-3 mb-8" />
<div className="grid grid-cols-1 @lg:grid-cols-2 @4xl:grid-cols-3 gap-4 min-w-0">
{children.map((child) => (
<ChildCard key={child.id} child={child} />
))}
</div>
</div>
);
}
function ChildCard({ child }: { child: Folder | HttpRequest | GrpcRequest | WebsocketRequest }) {
let card;
if (child.model === 'folder') {
card = <FolderCard folder={child} />;
} else if (child.model === 'http_request') {
card = <HttpRequestCard request={child} />;
} else if (child.model === 'grpc_request') {
card = <RequestCard request={child} />;
} else if (child.model === 'websocket_request') {
card = <RequestCard request={child} />;
} else {
card = <div>Unknown model {child['model']}</div>;
}
const navigate = useCallback(async () => {
await router.navigate({
to: '/workspaces/$workspaceId',
params: { workspaceId: child.workspaceId },
search: (prev) => ({ ...prev, request_id: child.id }),
});
}, [child.id, child.workspaceId]);
return (
<div
className={classNames(
'rounded-lg bg-surface-highlight p-3 pt-1 border border-border',
'flex flex-col gap-3',
)}
>
<HStack space={2}>
{child.model === 'folder' && <Icon icon="folder" size="lg" />}
<Heading className="truncate" level={2}>
{resolvedModelName(child)}
</Heading>
<HStack space={0.5} className="ml-auto -mr-1.5">
<IconButton
color="custom"
title="Send Request"
size="sm"
icon="external_link"
className="opacity-70 hover:opacity-100"
onClick={navigate}
/>
<IconButton
color="custom"
title="Send Request"
size="sm"
icon="send_horizontal"
className="opacity-70 hover:opacity-100"
onClick={() => {
sendAnyHttpRequest.mutate(child.id);
}}
/>
</HStack>
</HStack>
<div className="text-text-subtle">{card}</div>
</div>
);
}
function FolderCard({ folder }: { folder: Folder }) {
return (
<div>
<Button
color="primary"
onClick={async () => {
await router.navigate({
to: '/workspaces/$workspaceId',
params: { workspaceId: folder.workspaceId },
search: (prev) => {
return { ...prev, request_id: null, folder_id: folder.id };
},
});
}}
>
Open
</Button>
</div>
);
}
function RequestCard({ request }: { request: HttpRequest | GrpcRequest | WebsocketRequest }) {
return <div>TODO {request.id}</div>;
}
function HttpRequestCard({ request }: { request: HttpRequest }) {
const latestResponse = useLatestHttpResponse(request.id);
return (
<div className="grid grid-rows-2 grid-cols-[minmax(0,1fr)] gap-2 overflow-hidden">
<code className="font-mono text-editor text-info border border-info rounded px-2.5 py-0.5 truncate w-full min-w-0">
{request.method} {request.url}
</code>
{latestResponse ? (
<button
className="block mr-auto"
type="button"
tabIndex={-1}
onClick={(e) => {
e.stopPropagation();
showDialog({
id: 'response-preview',
title: 'Response Preview',
size: 'md',
className: 'h-full',
render: () => {
return <HttpResponsePane activeRequestId={request.id} />;
},
});
}}
>
<HStack
space={2}
alignItems="center"
className={classNames(
'cursor-default select-none',
'whitespace-nowrap w-full pl-3 overflow-x-auto font-mono text-sm hide-scrollbars',
'font-mono text-editor border rounded px-1.5 py-0.5 truncate w-full',
)}
>
{latestResponse.state !== 'closed' && <LoadingIcon size="sm" />}
<HttpStatusTag showReason response={latestResponse} />
<span>&bull;</span>
<HttpResponseDurationTag response={latestResponse} />
<span>&bull;</span>
<SizeTag contentLength={latestResponse.contentLength ?? 0} />
</HStack>
</button>
) : (
<div>No Responses</div>
)}
</div>
);
}

View File

@@ -68,14 +68,15 @@ export function FolderSettingsDialog({ folderId, tab }: Props) {
value={activeTab}
onChangeValue={setActiveTab}
label="Folder Settings"
className="px-1.5 pb-2"
className="pt-2 pb-2 pl-3 pr-1"
layout="horizontal"
addBorders
tabs={tabs}
>
<TabContent value={TAB_AUTH} className="pt-3 overflow-y-auto h-full px-4">
<TabContent value={TAB_AUTH} className="overflow-y-auto h-full px-4">
<HttpAuthenticationEditor model={folder} />
</TabContent>
<TabContent value={TAB_GENERAL} className="pt-3 overflow-y-auto h-full px-4">
<TabContent value={TAB_GENERAL} className="overflow-y-auto h-full px-4">
<VStack space={3} className="pb-3 h-full">
<Input
label="Folder Name"
@@ -93,7 +94,7 @@ export function FolderSettingsDialog({ folderId, tab }: Props) {
/>
</VStack>
</TabContent>
<TabContent value={TAB_HEADERS} className="pt-3 overflow-y-auto h-full px-4">
<TabContent value={TAB_HEADERS} className="overflow-y-auto h-full px-4">
<HeadersEditor
inheritedHeaders={inheritedHeaders}
forceUpdateKey={folder.id}
@@ -102,7 +103,7 @@ export function FolderSettingsDialog({ folderId, tab }: Props) {
stateKey={`headers.${folder.id}`}
/>
</TabContent>
<TabContent value={TAB_VARIABLES} className="pt-3 overflow-y-auto h-full px-4">
<TabContent value={TAB_VARIABLES} className="overflow-y-auto h-full px-4">
{folderEnvironment == null ? (
<EmptyStateText>
<VStack alignItems="center" space={1.5}>

View File

@@ -1,10 +1,14 @@
import { activeRequestAtom } from '../hooks/useActiveRequest';
import { useSubscribeActiveWorkspaceId } from '../hooks/useActiveWorkspace';
import { useActiveWorkspaceChangedToast } from '../hooks/useActiveWorkspaceChangedToast';
import { useHotKey, useSubscribeHotKeys } from '../hooks/useHotKey';
import { useSubscribeHttpAuthentication } from '../hooks/useHttpAuthentication';
import { useSyncFontSizeSetting } from '../hooks/useSyncFontSizeSetting';
import { useSyncWorkspaceChildModels } from '../hooks/useSyncWorkspaceChildModels';
import { useSyncZoomSetting } from '../hooks/useSyncZoomSetting';
import { useSubscribeTemplateFunctions } from '../hooks/useTemplateFunctions';
import { jotaiStore } from '../lib/jotai';
import { renameModelWithPrompt } from '../lib/renameModelWithPrompt';
export function GlobalHooks() {
useSyncZoomSetting();
@@ -18,6 +22,17 @@ export function GlobalHooks() {
// Other useful things
useActiveWorkspaceChangedToast();
useSubscribeHotKeys();
useHotKey(
'request.rename',
async () => {
const model = jotaiStore.get(activeRequestAtom);
if (model == null) return;
await renameModelWithPrompt(model);
},
{ allowDefault: true },
);
return null;
}

View File

@@ -117,7 +117,7 @@ export function GrpcConnectionLayout({ style }: Props) {
) : grpcEvents.length >= 0 ? (
<GrpcResponsePane activeRequest={activeRequest} methodType={methodType} />
) : (
<HotKeyList hotkeys={['grpc_request.send', 'sidebar.focus', 'url_bar.focus']} />
<HotKeyList hotkeys={['request.send', 'sidebar.focus', 'url_bar.focus']} />
)}
</div>
)

View File

@@ -17,7 +17,7 @@ import { showDialog } from '../lib/dialog';
import { pluralizeCount } from '../lib/pluralize';
import { Button } from './core/Button';
import type { EditorProps } from './core/Editor/Editor';
import { Editor } from './core/Editor/Editor';
import { Editor } from './core/Editor/LazyEditor';
import { FormattedError } from './core/FormattedError';
import { InlineCode } from './core/InlineCode';
import { VStack } from './core/Stacks';

View File

@@ -60,7 +60,7 @@ function GrpcProtoSelectionDialogWithRequest({ request }: Props & { request: Grp
await grpc.reflect.refetch();
}}
>
Add Files
Add Proto Files
</Button>
<Button
variant="border"
@@ -76,7 +76,7 @@ function GrpcProtoSelectionDialogWithRequest({ request }: Props & { request: Grp
await grpc.reflect.refetch();
}}
>
Add Directories
Add Import Folders
</Button>
<Button
isLoading={grpc.reflect.isFetching}

View File

@@ -240,7 +240,7 @@ export function GrpcRequestPane({
size="sm"
variant="border"
title={isStreaming ? 'Connect' : 'Send'}
hotkeyAction="grpc_request.send"
hotkeyAction="request.send"
onClick={isStreaming ? handleSend : handleConnect}
icon={isStreaming ? 'send_horizontal' : 'arrow_up_down'}
/>
@@ -250,7 +250,7 @@ export function GrpcRequestPane({
size="sm"
variant="border"
title={methodType === 'unary' ? 'Send' : 'Connect'}
hotkeyAction="grpc_request.send"
hotkeyAction="request.send"
onClick={isStreaming ? onCancel : handleConnect}
disabled={methodType === 'no-schema' || methodType === 'no-method'}
icon={

View File

@@ -15,7 +15,7 @@ import { copyToClipboard } from '../lib/copy';
import { AutoScroller } from './core/AutoScroller';
import { Banner } from './core/Banner';
import { Button } from './core/Button';
import { Editor } from './core/Editor/Editor';
import { Editor } from './core/Editor/LazyEditor';
import { HotKeyList } from './core/HotKeyList';
import { Icon } from './core/Icon';
import { IconButton } from './core/IconButton';
@@ -74,7 +74,7 @@ export function GrpcResponsePane({ style, methodType, activeRequest }: Props) {
firstSlot={() =>
activeConnection == null ? (
<HotKeyList
hotkeys={['http_request.send', 'http_request.create', 'sidebar.focus', 'url_bar.focus']}
hotkeys={['request.send', 'model.create', 'sidebar.focus', 'url_bar.focus']}
/>
) : (
<div className="w-full grid grid-rows-[auto_minmax(0,1fr)] grid-cols-1 items-center">

View File

@@ -53,9 +53,6 @@ export function HeadersEditor({
disabled
disableDrag
className="py-1"
onChange={() => {}}
onEnd={() => {}}
onMove={() => {}}
pair={ensurePairId(pair)}
stateKey={null}
nameAutocompleteFunctions

View File

@@ -4,7 +4,7 @@ import type { GenericCompletionOption } from '@yaakapp-internal/plugins';
import classNames from 'classnames';
import { atom, useAtomValue } from 'jotai';
import type { CSSProperties } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import React, { lazy, Suspense, useCallback, useMemo, useState } from 'react';
import { activeRequestIdAtom } from '../hooks/useActiveRequestId';
import { allRequestsAtom } from '../hooks/useAllRequests';
import { useAuthTab } from '../hooks/useAuthTab';
@@ -37,7 +37,7 @@ import { showToast } from '../lib/toast';
import { BinaryFileEditor } from './BinaryFileEditor';
import { ConfirmLargeRequestBody } from './ConfirmLargeRequestBody';
import { CountBadge } from './core/CountBadge';
import { Editor } from './core/Editor/Editor';
import { Editor } from './core/Editor/LazyEditor';
import type { GenericCompletionConfig } from './core/Editor/genericCompletion';
import { InlineCode } from './core/InlineCode';
import type { Pair } from './core/PairEditor';
@@ -53,7 +53,10 @@ import { MarkdownEditor } from './MarkdownEditor';
import { RequestMethodDropdown } from './RequestMethodDropdown';
import { UrlBar } from './UrlBar';
import { UrlParametersEditor } from './UrlParameterEditor';
import { GraphQLEditor } from './graphql/GraphQLEditor';
const GraphQLEditor = lazy(() =>
import('./graphql/GraphQLEditor').then((m) => ({ default: m.GraphQLEditor })),
);
interface Props {
style: CSSProperties;
@@ -160,7 +163,11 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
{ label: 'GraphQL', value: BODY_TYPE_GRAPHQL },
{ label: 'JSON', value: BODY_TYPE_JSON },
{ label: 'XML', value: BODY_TYPE_XML },
{ label: 'Other', value: BODY_TYPE_OTHER },
{
label: 'Other',
value: BODY_TYPE_OTHER,
shortLabel: nameOfContentTypeOr(contentType, 'Other'),
},
{ type: 'separator', label: 'Other' },
{ label: 'Binary File', value: BODY_TYPE_BINARY },
{ label: 'No Body', shortLabel: 'Body', value: BODY_TYPE_NONE },
@@ -229,6 +236,7 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
[
activeRequest,
authTab,
contentType,
handleContentTypeChange,
headersTab,
numParams,
@@ -400,12 +408,14 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
stateKey={`xml.${activeRequest.id}`}
/>
) : activeRequest.bodyType === BODY_TYPE_GRAPHQL ? (
<GraphQLEditor
forceUpdateKey={forceUpdateKey}
baseRequest={activeRequest}
request={activeRequest}
onChange={handleBodyChange}
/>
<Suspense>
<GraphQLEditor
forceUpdateKey={forceUpdateKey}
baseRequest={activeRequest}
request={activeRequest}
onChange={handleBodyChange}
/>
</Suspense>
) : activeRequest.bodyType === BODY_TYPE_FORM_URLENCODED ? (
<FormUrlencodedEditor
forceUpdateKey={forceUpdateKey}
@@ -471,3 +481,11 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
</div>
);
}
function nameOfContentTypeOr(contentType: string | null, fallback: string) {
const language = languageFromContentType(contentType);
if (language === 'markdown') {
return 'Markdown';
}
return fallback;
}

View File

@@ -1,7 +1,7 @@
import type { HttpResponse } from '@yaakapp-internal/models';
import classNames from 'classnames';
import type { CSSProperties, ReactNode } from 'react';
import React, { useCallback, useMemo } from 'react';
import type { ComponentType, CSSProperties } from 'react';
import React, { lazy, Suspense, useCallback, useMemo } from 'react';
import { useLocalStorage } from 'react-use';
import { useCancelHttpResponse } from '../hooks/useCancelHttpResponse';
import { usePinnedHttpResponse } from '../hooks/usePinnedHttpResponse';
@@ -10,16 +10,18 @@ import { getMimeTypeFromContentType } from '../lib/contentType';
import { getContentTypeFromHeaders } from '../lib/model_util';
import { ConfirmLargeResponse } from './ConfirmLargeResponse';
import { Banner } from './core/Banner';
import { Button } from './core/Button';
import { CountBadge } from './core/CountBadge';
import { HttpResponseDurationTag } from './core/HttpResponseDurationTag';
import { HotKeyList } from './core/HotKeyList';
import { HttpResponseDurationTag } from './core/HttpResponseDurationTag';
import { HttpStatusTag } from './core/HttpStatusTag';
import { LoadingIcon } from './core/LoadingIcon';
import { SizeTag } from './core/SizeTag';
import { HStack, VStack } from './core/Stacks';
import { HttpStatusTag } from './core/HttpStatusTag';
import type { TabItem } from './core/Tabs/Tabs';
import { TabContent, Tabs } from './core/Tabs/Tabs';
import type { TabItem} from './core/Tabs/Tabs';
import { Tabs , TabContent} from './core/Tabs/Tabs';
import { EmptyStateText } from './EmptyStateText';
import { ErrorBoundary } from './ErrorBoundary';
import { RecentHttpResponsesDropdown } from './RecentHttpResponsesDropdown';
import { ResponseHeaders } from './ResponseHeaders';
import { ResponseInfo } from './ResponseInfo';
@@ -28,11 +30,12 @@ import { CsvViewer } from './responseViewers/CsvViewer';
import { EventStreamViewer } from './responseViewers/EventStreamViewer';
import { HTMLOrTextViewer } from './responseViewers/HTMLOrTextViewer';
import { ImageViewer } from './responseViewers/ImageViewer';
import { PdfViewer } from './responseViewers/PdfViewer';
import { SvgViewer } from './responseViewers/SvgViewer';
import { VideoViewer } from './responseViewers/VideoViewer';
import { ErrorBoundary } from './ErrorBoundary';
import { Button } from './core/Button';
const PdfViewer = lazy(() =>
import('./responseViewers/PdfViewer').then((m) => ({ default: m.PdfViewer })),
);
interface Props {
style?: CSSProperties;
@@ -106,9 +109,7 @@ export function HttpResponsePane({ style, className, activeRequestId }: Props) {
)}
>
{activeResponse == null ? (
<HotKeyList
hotkeys={['http_request.send', 'http_request.create', 'sidebar.focus', 'url_bar.focus']}
/>
<HotKeyList hotkeys={['request.send', 'model.create', 'sidebar.focus', 'url_bar.focus']} />
) : (
<div className="h-full w-full grid grid-rows-[auto_minmax(0,1fr)] grid-cols-1">
<HStack
@@ -161,41 +162,46 @@ export function HttpResponsePane({ style, className, activeRequestId }: Props) {
>
<TabContent value={TAB_BODY}>
<ErrorBoundary name="Http Response Viewer">
<ConfirmLargeResponse response={activeResponse}>
{activeResponse.state === 'initialized' ? (
<EmptyStateText>
<VStack space={3}>
<HStack space={3}>
<LoadingIcon className="text-text-subtlest" />
Sending Request
</HStack>
<Button size="sm" variant="border" onClick={() => cancel.mutate()}>Cancel</Button>
</VStack>
</EmptyStateText>
) : activeResponse.state === 'closed' && activeResponse.contentLength === 0 ? (
<EmptyStateText>Empty </EmptyStateText>
) : mimeType?.match(/^text\/event-stream/i) && viewMode === 'pretty' ? (
<EventStreamViewer response={activeResponse} />
) : mimeType?.match(/^image\/svg/) ? (
<SvgViewer response={activeResponse} />
) : mimeType?.match(/^image/i) ? (
<EnsureCompleteResponse response={activeResponse} render={ImageViewer} />
) : mimeType?.match(/^audio/i) ? (
<EnsureCompleteResponse response={activeResponse} render={AudioViewer} />
) : mimeType?.match(/^video/i) ? (
<EnsureCompleteResponse response={activeResponse} render={VideoViewer} />
) : mimeType?.match(/pdf/i) ? (
<EnsureCompleteResponse response={activeResponse} render={PdfViewer} />
) : mimeType?.match(/csv|tab-separated/i) ? (
<CsvViewer className="pb-2" response={activeResponse} />
) : (
<HTMLOrTextViewer
textViewerClassName="-mr-2 bg-surface" // Pull to the right
response={activeResponse}
pretty={viewMode === 'pretty'}
/>
)}
</ConfirmLargeResponse>
<Suspense>
<ConfirmLargeResponse response={activeResponse}>
{activeResponse.state === 'initialized' ? (
<EmptyStateText>
<VStack space={3}>
<HStack space={3}>
<LoadingIcon className="text-text-subtlest" />
Sending Request
</HStack>
<Button size="sm" variant="border" onClick={() => cancel.mutate()}>
Cancel
</Button>
</VStack>
</EmptyStateText>
) : activeResponse.state === 'closed' &&
activeResponse.contentLength === 0 ? (
<EmptyStateText>Empty </EmptyStateText>
) : mimeType?.match(/^text\/event-stream/i) && viewMode === 'pretty' ? (
<EventStreamViewer response={activeResponse} />
) : mimeType?.match(/^image\/svg/) ? (
<SvgViewer response={activeResponse} />
) : mimeType?.match(/^image/i) ? (
<EnsureCompleteResponse response={activeResponse} Component={ImageViewer} />
) : mimeType?.match(/^audio/i) ? (
<EnsureCompleteResponse response={activeResponse} Component={AudioViewer} />
) : mimeType?.match(/^video/i) ? (
<EnsureCompleteResponse response={activeResponse} Component={VideoViewer} />
) : mimeType?.match(/pdf/i) ? (
<EnsureCompleteResponse response={activeResponse} Component={PdfViewer} />
) : mimeType?.match(/csv|tab-separated/i) ? (
<CsvViewer className="pb-2" response={activeResponse} />
) : (
<HTMLOrTextViewer
textViewerClassName="-mr-2 bg-surface" // Pull to the right
response={activeResponse}
pretty={viewMode === 'pretty'}
/>
)}
</ConfirmLargeResponse>
</Suspense>
</ErrorBoundary>
</TabContent>
<TabContent value={TAB_HEADERS}>
@@ -214,10 +220,10 @@ export function HttpResponsePane({ style, className, activeRequestId }: Props) {
function EnsureCompleteResponse({
response,
render,
Component,
}: {
response: HttpResponse;
render: (v: { bodyPath: string }) => ReactNode;
Component: ComponentType<{ bodyPath: string }>;
}) {
if (response.bodyPath === null) {
return <div>Empty response body</div>;
@@ -232,5 +238,5 @@ function EnsureCompleteResponse({
);
}
return render({ bodyPath: response.bodyPath });
return <Component bodyPath={response.bodyPath} />;
}

View File

@@ -14,7 +14,6 @@ export function LocalImage({ src: srcPath, className }: Props) {
queryKey: ['local-image', srcPath],
queryFn: async () => {
const p = await resolveResource(srcPath);
console.log("LOADING SRC", srcPath, p)
return convertFileSrc(p);
},
});

View File

@@ -1,7 +1,7 @@
import classNames from 'classnames';
import { useRef, useState } from 'react';
import type { EditorProps } from './core/Editor/Editor';
import { Editor } from './core/Editor/Editor';
import { Editor } from './core/Editor/LazyEditor';
import { SegmentedControl } from './core/SegmentedControl';
import { Markdown } from './Markdown';

View File

@@ -0,0 +1,497 @@
import type {
Folder,
GrpcRequest,
HttpRequest,
WebsocketRequest,
Workspace,
} from '@yaakapp-internal/models';
import {
duplicateModel,
foldersAtom,
getModel,
grpcConnectionsAtom,
httpResponsesAtom,
patchModel,
websocketConnectionsAtom,
workspacesAtom,
} from '@yaakapp-internal/models';
import classNames from 'classnames';
import { atom, useAtomValue } from 'jotai';
import { selectAtom } from 'jotai/utils';
import React, { memo, useCallback, useEffect, useMemo, useRef } from 'react';
import { moveToWorkspace } from '../commands/moveToWorkspace';
import { openFolderSettings } from '../commands/openFolderSettings';
import { activeCookieJarAtom } from '../hooks/useActiveCookieJar';
import { activeEnvironmentAtom } from '../hooks/useActiveEnvironment';
import { activeFolderIdAtom } from '../hooks/useActiveFolderId';
import { activeRequestIdAtom } from '../hooks/useActiveRequestId';
import { activeWorkspaceAtom, activeWorkspaceIdAtom } from '../hooks/useActiveWorkspace';
import { allRequestsAtom } from '../hooks/useAllRequests';
import { getCreateDropdownItems } from '../hooks/useCreateDropdownItems';
import { getGrpcRequestActions } from '../hooks/useGrpcRequestActions';
import { useHotKey } from '../hooks/useHotKey';
import { getHttpRequestActions } from '../hooks/useHttpRequestActions';
import { sendAnyHttpRequest } from '../hooks/useSendAnyHttpRequest';
import { useSidebarHidden } from '../hooks/useSidebarHidden';
import { deepEqualAtom } from '../lib/atoms';
import { deleteModelWithConfirm } from '../lib/deleteModelWithConfirm';
import { jotaiStore } from '../lib/jotai';
import { renameModelWithPrompt } from '../lib/renameModelWithPrompt';
import { resolvedModelName } from '../lib/resolvedModelName';
import { isSidebarFocused } from '../lib/scopes';
import { navigateToRequestOrFolderOrWorkspace } from '../lib/setWorkspaceSearchParams';
import { invokeCmd } from '../lib/tauri';
import type { ContextMenuProps, DropdownItem } from './core/Dropdown';
import { HttpMethodTag } from './core/HttpMethodTag';
import { HttpStatusTag } from './core/HttpStatusTag';
import { Icon } from './core/Icon';
import { LoadingIcon } from './core/LoadingIcon';
import { isSelectedFamily } from './core/tree/atoms';
import type { TreeNode } from './core/tree/common';
import type { TreeHandle, TreeProps } from './core/tree/Tree';
import { Tree } from './core/tree/Tree';
import type { TreeItemProps } from './core/tree/TreeItem';
import { GitDropdown } from './GitDropdown';
type SidebarModel = Workspace | Folder | HttpRequest | GrpcRequest | WebsocketRequest;
const OPACITY_SUBTLE = 'opacity-80';
function NewSidebar({ className }: { className?: string }) {
const [hidden, setHidden] = useSidebarHidden();
const tree = useAtomValue(sidebarTreeAtom);
const activeWorkspaceId = useAtomValue(activeWorkspaceAtom)?.id;
const treeId = 'tree.' + (activeWorkspaceId ?? 'unknown');
const wrapperRef = useRef<HTMLElement>(null);
const treeRef = useRef<TreeHandle>(null);
const focusActiveItem = useCallback(() => {
treeRef.current?.focus();
}, []);
useHotKey('sidebar.focus', async function focusHotkey() {
// Hide the sidebar if it's already focused
if (!hidden && isSidebarFocused()) {
await setHidden(true);
return;
}
// Show the sidebar if it's hidden
if (hidden) {
await setHidden(false);
}
// Select the 0th index on focus if none selected
focusActiveItem();
});
const handleDragEnd = useCallback(async function handleDragEnd({
items,
parent,
children,
insertAt,
}: {
items: SidebarModel[];
parent: SidebarModel;
children: SidebarModel[];
insertAt: number;
}) {
const prev = children[insertAt - 1] as Exclude<SidebarModel, Workspace>;
const next = children[insertAt] as Exclude<SidebarModel, Workspace>;
const folderId = parent.model === 'folder' ? parent.id : null;
const beforePriority = prev?.sortPriority ?? 0;
const afterPriority = next?.sortPriority ?? 0;
const shouldUpdateAll = afterPriority - beforePriority < 1;
try {
if (shouldUpdateAll) {
// Add items to children at insertAt
children.splice(insertAt, 0, ...items);
await Promise.all(
children.map((m, i) => patchModel(m, { sortPriority: i * 1000, folderId })),
);
} else {
const range = afterPriority - beforePriority;
const increment = range / (items.length + 2);
await Promise.all(
items.map((m, i) =>
// Spread item sortPriority out over before/after range
patchModel(m, { sortPriority: beforePriority + (i + 1) * increment, folderId }),
),
);
}
} catch (e) {
console.error(e);
}
}, []);
const handleTreeRefInit = useCallback((n: TreeHandle) => {
treeRef.current = n;
if (n == null) return;
const activeId = jotaiStore.get(activeIdAtom);
if (activeId == null) return;
n.selectItem(activeId);
}, []);
useEffect(() => {
return jotaiStore.sub(activeIdAtom, () => {
const activeId = jotaiStore.get(activeIdAtom);
if (activeId == null) return;
treeRef.current?.selectItem(activeId);
});
}, []);
if (tree == null || hidden) {
return null;
}
return (
<aside
ref={wrapperRef}
aria-hidden={hidden ?? undefined}
className={classNames(className, 'h-full grid grid-rows-[minmax(0,1fr)_auto]')}
>
<Tree
ref={handleTreeRefInit}
root={tree}
treeId={treeId}
hotkeys={hotkeys}
getItemKey={getItemKey}
ItemInner={SidebarInnerItem}
ItemLeftSlot={SidebarLeftSlot}
getContextMenu={getContextMenu}
onActivate={handleActivate}
getEditOptions={getEditOptions}
className="pl-2 pr-3 pt-2 pb-2"
onDragEnd={handleDragEnd}
/>
<GitDropdown />
</aside>
);
}
export default NewSidebar;
const activeIdAtom = atom<string | null>((get) => {
return get(activeRequestIdAtom) || get(activeFolderIdAtom);
});
function getEditOptions(
item: SidebarModel,
): ReturnType<NonNullable<TreeItemProps<SidebarModel>['getEditOptions']>> {
return {
onChange: handleSubmitEdit,
defaultValue: resolvedModelName(item),
placeholder: item.name,
};
}
async function handleSubmitEdit(item: SidebarModel, text: string) {
await patchModel(item, { name: text });
}
function handleActivate(item: SidebarModel) {
// TODO: Add folder layout support
if (item.model !== 'folder' && item.model !== 'workspace') {
navigateToRequestOrFolderOrWorkspace(item.id, item.model);
}
}
const allPotentialChildrenAtom = atom<SidebarModel[]>((get) => {
const requests = get(allRequestsAtom);
const folders = get(foldersAtom);
return [...requests, ...folders];
});
const memoAllPotentialChildrenAtom = deepEqualAtom(allPotentialChildrenAtom);
const sidebarTreeAtom = atom((get) => {
const allModels = get(memoAllPotentialChildrenAtom);
const activeWorkspace = get(activeWorkspaceAtom);
const childrenMap: Record<string, Exclude<SidebarModel, Workspace>[]> = {};
for (const item of allModels) {
if ('folderId' in item && item.folderId == null) {
childrenMap[item.workspaceId] = childrenMap[item.workspaceId] ?? [];
childrenMap[item.workspaceId]!.push(item);
} else if ('folderId' in item && item.folderId != null) {
childrenMap[item.folderId] = childrenMap[item.folderId] ?? [];
childrenMap[item.folderId]!.push(item);
}
}
const treeParentMap: Record<string, TreeNode<SidebarModel>> = {};
if (activeWorkspace == null) {
return null;
}
// Put requests and folders into a tree structure
const next = (node: TreeNode<SidebarModel>, depth: number): TreeNode<SidebarModel> => {
const childItems = childrenMap[node.item.id] ?? [];
// Recurse to children
childItems.sort((a, b) => a.sortPriority - b.sortPriority);
if (node.item.model === 'folder' || node.item.model === 'workspace') {
node.children = node.children ?? [];
for (const item of childItems) {
treeParentMap[item.id] = node;
node.children.push(next({ item, parent: node, depth }, depth + 1));
}
}
return node;
};
return next(
{
item: activeWorkspace,
children: [],
parent: null,
depth: 0,
},
1,
);
});
const actions = {
'sidebar.selected.delete': {
enable: isSidebarFocused,
cb: async function (_: TreeHandle, items: SidebarModel[]) {
await deleteModelWithConfirm(items);
},
},
'sidebar.selected.rename': {
enable: isSidebarFocused,
allowDefault: true,
cb: async function (tree: TreeHandle, items: SidebarModel[]) {
const item = items[0];
if (items.length === 1 && item != null) {
tree.renameItem(item.id);
}
},
},
'sidebar.selected.duplicate': {
priority: 999,
enable: isSidebarFocused,
cb: async function (_: TreeHandle, items: SidebarModel[]) {
if (items.length === 1) {
const item = items[0]!;
const newId = await duplicateModel(item);
navigateToRequestOrFolderOrWorkspace(newId, item.model);
} else {
await Promise.all(items.map(duplicateModel));
}
},
},
'request.send': {
enable: isSidebarFocused,
cb: async function (_: TreeHandle, items: SidebarModel[]) {
await Promise.all(
items.filter((i) => i.model === 'http_request').map((i) => sendAnyHttpRequest.mutate(i.id)),
);
},
},
} as const;
const hotkeys: TreeProps<SidebarModel>['hotkeys'] = { actions };
async function getContextMenu(tree: TreeHandle, items: SidebarModel[]): Promise<DropdownItem[]> {
const workspaceId = jotaiStore.get(activeWorkspaceIdAtom);
const child = items[0];
// No children means we're in the root
if (child == null) {
return getCreateDropdownItems({ workspaceId, activeRequest: null, folderId: null });
}
const workspaces = jotaiStore.get(workspacesAtom);
const onlyHttpRequests = items.every((i) => i.model === 'http_request');
const initialItems: ContextMenuProps['items'] = [
{
label: 'Folder Settings',
hidden: !(items.length === 1 && child.model === 'folder'),
leftSlot: <Icon icon="folder_cog" />,
onSelect: () => openFolderSettings(child.id),
},
{
label: 'Send All',
hidden: !(items.length === 1 && child.model === 'folder'),
leftSlot: <Icon icon="send_horizontal" />,
onSelect: () => {
const environment = jotaiStore.get(activeEnvironmentAtom);
const cookieJar = jotaiStore.get(activeCookieJarAtom);
invokeCmd('cmd_send_folder', {
folderId: child.id,
environmentId: environment?.id,
cookieJarId: cookieJar?.id,
});
},
},
{
label: 'Send',
hotKeyAction: 'request.send',
hotKeyLabelOnly: true,
hidden: !onlyHttpRequests,
leftSlot: <Icon icon="send_horizontal" />,
onSelect: () => actions['request.send'].cb(tree, items),
},
...(items.length === 1 && child.model === 'http_request'
? await getHttpRequestActions()
: []
).map((a) => ({
label: a.label,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
leftSlot: <Icon icon={(a.icon as any) ?? 'empty'} />,
onSelect: async () => {
const request = getModel('http_request', child.id);
if (request != null) await a.call(request);
},
})),
...(items.length === 1 && child.model === 'grpc_request'
? await getGrpcRequestActions()
: []
).map((a) => ({
label: a.label,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
leftSlot: <Icon icon={(a.icon as any) ?? 'empty'} />,
onSelect: async () => {
const request = getModel('grpc_request', child.id);
if (request != null) await a.call(request);
},
})),
];
const modelCreationItems: DropdownItem[] =
items.length === 1 && child.model === 'folder'
? [
{ type: 'separator' },
...getCreateDropdownItems({ workspaceId, activeRequest: null, folderId: child.id }),
]
: [];
const menuItems: ContextMenuProps['items'] = [
...initialItems,
{ type: 'separator', hidden: initialItems.filter((v) => !v.hidden).length === 0 },
{
label: 'Rename',
leftSlot: <Icon icon="pencil" />,
hidden: items.length > 1,
hotKeyAction: 'sidebar.selected.rename',
hotKeyLabelOnly: true,
onSelect: async () => {
const request = getModel(
['folder', 'http_request', 'grpc_request', 'websocket_request'],
child.id,
);
await renameModelWithPrompt(request);
},
},
{
label: 'Duplicate',
hotKeyAction: 'model.duplicate',
hotKeyLabelOnly: true, // Would trigger for every request (bad)
leftSlot: <Icon icon="copy" />,
onSelect: () => actions['sidebar.selected.duplicate'].cb(tree, items),
},
{
label: 'Move',
leftSlot: <Icon icon="arrow_right_circle" />,
hidden:
workspaces.length <= 1 ||
items.length > 1 ||
child.model === 'folder' ||
child.model === 'workspace',
onSelect: () => {
if (child.model === 'folder' || child.model === 'workspace') return;
moveToWorkspace.mutate(child);
},
},
{
color: 'danger',
label: 'Delete',
hotKeyAction: 'sidebar.selected.delete',
hotKeyLabelOnly: true,
leftSlot: <Icon icon="trash" />,
onSelect: () => actions['sidebar.selected.delete'].cb(tree, items),
},
...modelCreationItems,
];
return menuItems;
}
function getItemKey(item: SidebarModel) {
const responses = jotaiStore.get(httpResponsesAtom);
const latestResponse = responses.find((r) => r.requestId === item.id) ?? null;
const url = 'url' in item ? item.url : 'n/a';
const method = 'method' in item ? item.method : 'n/a';
return [
item.id,
item.name,
url,
method,
latestResponse?.elapsed,
latestResponse?.id ?? 'n/a',
].join('::');
}
const SidebarLeftSlot = memo(function SidebarLeftSlot({
treeId,
item,
}: {
treeId: string;
item: SidebarModel;
}) {
if (item.model === 'folder') {
return <Icon icon="folder" />;
} else if (item.model === 'workspace') {
return null;
} else {
const isSelected = jotaiStore.get(isSelectedFamily({ treeId, itemId: item.id }));
return (
<HttpMethodTag
short
className={classNames('text-xs', !isSelected && OPACITY_SUBTLE)}
request={item}
/>
);
}
});
const SidebarInnerItem = memo(function SidebarInnerItem({
item,
}: {
treeId: string;
item: SidebarModel;
}) {
const response = useAtomValue(
useMemo(
() =>
selectAtom(
atom((get) => [
...get(grpcConnectionsAtom),
...get(httpResponsesAtom),
...get(websocketConnectionsAtom),
]),
(responses) => responses.find((r) => r.requestId === item.id),
(a, b) => a?.state === b?.state && a?.id === b?.id, // Only update when the response state changes updated
),
[item.id],
),
);
return (
<div className="flex items-center gap-2 min-w-0 h-full w-full text-left">
<div className="truncate">{resolvedModelName(item)}</div>
{response != null && (
<div className="ml-auto">
{response.state !== 'closed' ? (
<LoadingIcon size="sm" className="text-text-subtlest" />
) : response.model === 'http_response' ? (
<HttpStatusTag short className="text-xs" response={response} />
) : null}
</div>
)}
</div>
);
});

View File

@@ -2,7 +2,7 @@ import classNames from 'classnames';
import { FocusTrap } from 'focus-trap-react';
import * as m from 'motion/react-m';
import type { ReactNode } from 'react';
import React from 'react';
import React, { useRef } from 'react';
import { Portal } from './Portal';
interface Props {
@@ -32,6 +32,8 @@ export function Overlay({
noBackdrop,
children,
}: Props) {
const containerRef = useRef<HTMLDivElement>(null);
if (noBackdrop) {
return (
<Portal name={portalName}>
@@ -44,15 +46,33 @@ export function Overlay({
</Portal>
);
}
return (
<Portal name={portalName}>
{open && (
<FocusTrap
focusTrapOptions={{
allowOutsideClick: true, // So we can still click toasts and things
delayInitialFocus: true,
fallbackFocus: () => containerRef.current!, // always have a target
initialFocus: () =>
// Doing this explicitly seems to work better than the default behavior for some reason
containerRef.current?.querySelector<HTMLElement>(
[
'a[href]',
'input:not([disabled])',
'select:not([disabled])',
'textarea:not([disabled])',
'button:not([disabled])',
'[tabindex]:not([tabindex="-1"])',
'[contenteditable]:not([contenteditable="false"])',
].join(', '),
) ?? undefined,
}}
>
<m.div
ref={containerRef}
tabIndex={-1}
className={classNames('fixed inset-0', zIndexes[zIndex])}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}

View File

@@ -32,7 +32,7 @@ export function RecentRequestsDropdown({ className }: Props) {
}
});
useHotKey('request_switcher.prev', () => {
useHotKey('switcher.prev', () => {
if (!dropdownRef.current?.isOpen) {
// Select the second because the first is the current request
dropdownRef.current?.open(1);
@@ -41,7 +41,7 @@ export function RecentRequestsDropdown({ className }: Props) {
}
});
useHotKey('request_switcher.next', () => {
useHotKey('switcher.next', () => {
if (!dropdownRef.current?.isOpen) dropdownRef.current?.open();
dropdownRef.current?.prev?.();
});
@@ -87,7 +87,7 @@ export function RecentRequestsDropdown({ className }: Props) {
<Dropdown ref={dropdownRef} items={items}>
<Button
size="sm"
hotkeyAction="request_switcher.toggle"
hotkeyAction="switcher.toggle"
className={classNames(
className,
'truncate pointer-events-auto',

View File

@@ -32,7 +32,7 @@ export function RedirectToLatestWorkspace() {
request_id: requestId,
};
console.log("Redirecting to workspace", params, search);
console.log('Redirecting to workspace', params, search);
await router.navigate({ to: '/workspaces/$workspaceId', params, search });
})();
}, [recentWorkspaces, workspaces, workspaces.length]);

View File

@@ -25,9 +25,8 @@ export function ResizeHandle({
return (
<div
aria-hidden
draggable
style={style}
onDragStart={onResizeStart}
onPointerDown={onResizeStart}
onDoubleClick={onReset}
className={classNames(
className,

View File

@@ -1,7 +1,9 @@
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
import { open } from '@tauri-apps/plugin-dialog';
import classNames from 'classnames';
import mime from 'mime';
import type { ReactNode } from 'react';
import { useEffect, useRef, useState } from 'react';
import type { ButtonProps } from './core/Button';
import { Button } from './core/Button';
import { IconButton } from './core/IconButton';
@@ -51,9 +53,43 @@ export function SelectFile({
const itemLabel = noun ?? (directory ? 'Folder' : 'File');
const selectOrChange = (filePath ? 'Change ' : 'Select ') + itemLabel;
const [isHovering, setIsHovering] = useState(false);
const ref = useRef<HTMLDivElement>(null);
// Listen for dropped files on the element
// NOTE: This doesn't work for Windows since native drag-n-drop can't work at the same tmie
// as browser drag-n-drop.
useEffect(() => {
let unlisten: (() => void) | undefined;
const setup = async () => {
const webview = getCurrentWebviewWindow();
unlisten = await webview.onDragDropEvent((event) => {
if (event.payload.type === 'over') {
const p = event.payload.position;
const r = ref.current?.getBoundingClientRect();
if (r == null) return;
const isOver = p.x >= r.left && p.x <= r.right && p.y >= r.top && p.y <= r.bottom;
console.log('IS OVER', isOver);
setIsHovering(isOver);
} else if (event.payload.type === 'drop' && isHovering) {
console.log('User dropped', event.payload.paths);
const p = event.payload.paths[0];
if (p) onChange({ filePath: p, contentType: null });
setIsHovering(false);
} else {
console.log('File drop cancelled');
setIsHovering(false);
}
});
};
setup().catch(console.error);
return () => {
if (unlisten) unlisten();
};
}, [isHovering, onChange]);
return (
<div>
<div ref={ref} className="w-full">
{label && (
<Label htmlFor={null} help={help}>
{label}
@@ -66,8 +102,9 @@ export function SelectFile({
'rtl mr-1.5',
inline && 'w-full',
filePath && inline && 'font-mono text-xs',
isHovering && '!border-notice',
)}
color="secondary"
color={isHovering ? 'primary' : 'secondary'}
onClick={handleClick}
size={size}
{...props}

View File

@@ -128,7 +128,7 @@ export function SettingsGeneral() {
<Checkbox
checked={workspace.settingValidateCertificates}
help="When disabled, skip validatation of server certificates, useful when interacting with self-signed certs."
help="When disabled, skip validation of server certificates, useful when interacting with self-signed certs."
title="Validate TLS Certificates"
onChange={(settingValidateCertificates) =>
patchModel(workspace, { settingValidateCertificates })

View File

@@ -44,7 +44,6 @@ export function SettingsPlugins() {
label="Plugins"
onChangeValue={setTab}
addBorders
tabListClassName="!-ml-3"
tabs={[
{ label: 'Discover', value: 'search' },
{
@@ -243,7 +242,7 @@ function PluginSearch() {
defaultValue={query}
/>
</HStack>
<div className="w-full h-full">
<div className="w-full h-full overflow-y-auto">
{results.data == null ? (
<EmptyStateText>
<LoadingIcon size="xl" className="text-text-subtlest" />

View File

@@ -1,11 +1,10 @@
import { patchModel, settingsAtom } from '@yaakapp-internal/models';
import { useAtomValue } from 'jotai';
import React from 'react';
import React, { lazy, Suspense } from 'react';
import { activeWorkspaceAtom } from '../../hooks/useActiveWorkspace';
import { useResolvedAppearance } from '../../hooks/useResolvedAppearance';
import { useResolvedTheme } from '../../hooks/useResolvedTheme';
import type { ButtonProps } from '../core/Button';
import { Editor } from '../core/Editor/Editor';
import type { IconProps } from '../core/Icon';
import { Icon } from '../core/Icon';
import { IconButton } from '../core/IconButton';
@@ -13,6 +12,8 @@ import type { SelectProps } from '../core/Select';
import { Select } from '../core/Select';
import { HStack, VStack } from '../core/Stacks';
const Editor = lazy(() => import('../core/Editor/Editor').then((m) => ({ default: m.Editor })));
const buttonColors: ButtonProps['color'][] = [
'primary',
'info',
@@ -144,17 +145,19 @@ export function SettingsTheme() {
/>
))}
</HStack>
<Editor
defaultValue={[
'let foo = { // Demo code editor',
' foo: ("bar" || "baz" ?? \'qux\'),',
' baz: [1, 10.2, null, false, true],',
'};',
].join('\n')}
heightMode="auto"
language="javascript"
stateKey={null}
/>
<Suspense>
<Editor
defaultValue={[
'let foo = { // Demo code editor',
' foo: ("bar" || "baz" ?? \'qux\'),',
' baz: [1, 10.2, null, false, true],',
'};',
].join('\n')}
heightMode="auto"
language="javascript"
stateKey={null}
/>
</Suspense>
</VStack>
</VStack>
);

View File

@@ -1,10 +1,10 @@
import { useMemo } from 'react';
import { useFloatingSidebarHidden } from '../../hooks/useFloatingSidebarHidden';
import { useShouldFloatSidebar } from '../../hooks/useShouldFloatSidebar';
import { useSidebarHidden } from '../../hooks/useSidebarHidden';
import { IconButton } from '../core/IconButton';
import { HStack } from '../core/Stacks';
import { CreateDropdown } from '../CreateDropdown';
import { useFloatingSidebarHidden } from '../hooks/useFloatingSidebarHidden';
import { useShouldFloatSidebar } from '../hooks/useShouldFloatSidebar';
import { useSidebarHidden } from '../hooks/useSidebarHidden';
import { IconButton } from './core/IconButton';
import { HStack } from './core/Stacks';
import { CreateDropdown } from './CreateDropdown';
export function SidebarActions() {
const floating = useShouldFloatSidebar();
@@ -31,7 +31,7 @@ export function SidebarActions() {
icon={hidden ? 'left_panel_hidden' : 'left_panel_visible'}
iconColor="secondary"
/>
<CreateDropdown hotKeyAction="http_request.create">
<CreateDropdown hotKeyAction="model.create">
<IconButton size="sm" icon="plus_circle" iconColor="secondary" title="Add Resource" />
</CreateDropdown>
</HStack>

View File

@@ -1,9 +1,17 @@
import type {
Folder,
GrpcRequest,
HttpRequest,
WebsocketRequest,
Workspace,
} from '@yaakapp-internal/models';
import type { TemplateFunction } from '@yaakapp-internal/plugins';
import type { FnArg, Tokens } from '@yaakapp-internal/templates';
import classNames from 'classnames';
import { useEffect, useMemo, useState } from 'react';
import { useDebouncedValue } from '../hooks/useDebouncedValue';
import { useRenderTemplate } from '../hooks/useRenderTemplate';
import { useTemplateFunctionConfig } from '../hooks/useTemplateFunctionConfig';
import {
templateTokensToString,
useTemplateTokensToString,
@@ -24,6 +32,7 @@ interface Props {
initialTokens: Tokens;
hide: () => void;
onChange: (insert: string) => void;
model: HttpRequest | GrpcRequest | WebsocketRequest | Folder | Workspace;
}
export function TemplateFunctionDialog({ initialTokens, templateFunction, ...props }: Props) {
@@ -84,14 +93,15 @@ export function TemplateFunctionDialog({ initialTokens, templateFunction, ...pro
}
function InitializedTemplateFunctionDialog({
templateFunction,
hide,
templateFunction: { name },
initialArgValues,
hide,
onChange,
model,
}: Omit<Props, 'initialTokens'> & {
initialArgValues: Record<string, string | boolean>;
}) {
const enablePreview = templateFunction.name !== 'secure';
const enablePreview = name !== 'secure';
const [showSecretsInPreview, toggleShowSecretsInPreview] = useToggle(false);
const [argValues, setArgValues] = useState<Record<string, string | boolean>>(initialArgValues);
@@ -112,15 +122,16 @@ function InitializedTemplateFunctionDialog({
type: 'tag',
val: {
type: 'fn',
name: templateFunction.name,
name,
args: argTokens,
},
},
],
};
}, [argValues, templateFunction.name]);
}, [argValues, name]);
const tagText = useTemplateTokensToString(tokens);
const templateFunction = useTemplateFunctionConfig(name, argValues, model).data;
const handleDone = () => {
if (tagText.data) {
@@ -134,7 +145,7 @@ function InitializedTemplateFunctionDialog({
const tooLarge = rendered.data ? rendered.data.length > 10000 : false;
const dataContainsSecrets = useMemo(() => {
for (const [name, value] of Object.entries(argValues)) {
const arg = templateFunction.args.find((a) => 'name' in a && a.name === name);
const arg = templateFunction?.args.find((a) => 'name' in a && a.name === name);
const isTextPassword = arg?.type === 'text' && arg.password;
if (isTextPassword && typeof value === 'string' && value && rendered.data?.includes(value)) {
return true;
@@ -145,6 +156,8 @@ function InitializedTemplateFunctionDialog({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [rendered.data]);
if (templateFunction == null) return null;
return (
<VStack
as="form"
@@ -155,7 +168,7 @@ function InitializedTemplateFunctionDialog({
handleDone();
}}
>
{templateFunction.name === 'secure' ? (
{name === 'secure' ? (
<PlainInput
required
label="Value"

View File

@@ -98,7 +98,7 @@ export const UrlBar = memo(function UrlBar({
className="w-8 mr-0.5 !h-full"
iconColor="secondary"
icon={isLoading ? 'x' : submitIcon}
hotkeyAction="http_request.send"
hotkeyAction="request.send"
onMouseDown={(e) => {
// Prevent the button from taking focus
e.preventDefault();

View File

@@ -25,7 +25,7 @@ import { generateId } from '../lib/generateId';
import { prepareImportQuerystring } from '../lib/prepareImportQuerystring';
import { resolvedModelName } from '../lib/resolvedModelName';
import { CountBadge } from './core/CountBadge';
import { Editor } from './core/Editor/Editor';
import { Editor } from './core/Editor/LazyEditor';
import type { GenericCompletionConfig } from './core/Editor/genericCompletion';
import { IconButton } from './core/IconButton';
import type { Pair } from './core/PairEditor';

View File

@@ -17,7 +17,7 @@ import { copyToClipboard } from '../lib/copy';
import { AutoScroller } from './core/AutoScroller';
import { Banner } from './core/Banner';
import { Button } from './core/Button';
import { Editor } from './core/Editor/Editor';
import { Editor } from './core/Editor/LazyEditor';
import { HotKeyList } from './core/HotKeyList';
import { Icon } from './core/Icon';
import { IconButton } from './core/IconButton';
@@ -72,7 +72,7 @@ export function WebsocketResponsePane({ activeRequest }: Props) {
firstSlot={() =>
activeConnection == null ? (
<HotKeyList
hotkeys={['http_request.send', 'http_request.create', 'sidebar.focus', 'url_bar.focus']}
hotkeys={['request.send', 'model.create', 'sidebar.focus', 'url_bar.focus']}
/>
) : (
<div className="w-full grid grid-rows-[auto_minmax(0,1fr)] items-center">

View File

@@ -12,6 +12,8 @@ import {
activeEnvironmentAtom,
useSubscribeActiveEnvironmentId,
} from '../hooks/useActiveEnvironment';
import { activeFolderAtom } from '../hooks/useActiveFolder';
import { useSubscribeActiveFolderId } from '../hooks/useActiveFolderId';
import { activeRequestAtom } from '../hooks/useActiveRequest';
import { useSubscribeActiveRequestId } from '../hooks/useActiveRequestId';
import { activeWorkspaceAtom } from '../hooks/useActiveWorkspace';
@@ -26,7 +28,7 @@ import { useSidebarHidden } from '../hooks/useSidebarHidden';
import { useSidebarWidth } from '../hooks/useSidebarWidth';
import { useSyncWorkspaceRequestTitle } from '../hooks/useSyncWorkspaceRequestTitle';
import { useToggleCommandPalette } from '../hooks/useToggleCommandPalette';
import { duplicateRequestAndNavigate } from '../lib/duplicateRequestAndNavigate';
import { duplicateRequestOrFolderAndNavigate } from '../lib/duplicateRequestOrFolderAndNavigate';
import { importData } from '../lib/importData';
import { jotaiStore } from '../lib/jotai';
import { Banner } from './core/Banner';
@@ -36,13 +38,14 @@ import { FeedbackLink } from './core/Link';
import { HStack } from './core/Stacks';
import { CreateDropdown } from './CreateDropdown';
import { ErrorBoundary } from './ErrorBoundary';
import { FolderLayout } from './FolderLayout';
import { GrpcConnectionLayout } from './GrpcConnectionLayout';
import { HeaderSize } from './HeaderSize';
import { HttpRequestLayout } from './HttpRequestLayout';
import NewSidebar from './NewSidebar';
import { Overlay } from './Overlay';
import { ResizeHandle } from './ResizeHandle';
import { Sidebar } from './sidebar/Sidebar';
import { SidebarActions } from './sidebar/SidebarActions';
import { SidebarActions } from './SidebarActions';
import { WebsocketRequestLayout } from './WebsocketRequestLayout';
import { WorkspaceHeader } from './WorkspaceHeader';
@@ -161,7 +164,7 @@ export function Workspace() {
<SidebarActions />
</HeaderSize>
<ErrorBoundary name="Sidebar (Floating)">
<Sidebar />
<NewSidebar />
</ErrorBoundary>
</m.div>
</Overlay>
@@ -169,7 +172,7 @@ export function Workspace() {
<>
<div style={side} className={classNames('x-theme-sidebar', 'overflow-hidden bg-surface')}>
<ErrorBoundary name="Sidebar">
<Sidebar className="border-r border-border-subtle" />
<NewSidebar className="border-r border-border-subtle" />
</ErrorBoundary>
</div>
<ResizeHandle
@@ -193,7 +196,7 @@ export function Workspace() {
style={environmentBgStyle}
className="absolute inset-0 opacity-5"
/>
<div // Add subtle border bottom
<div // Add a subtle border bottom
style={environmentBgStyle}
className="absolute left-0 right-0 bottom-0 h-[0.5px] opacity-20"
/>
@@ -209,6 +212,7 @@ export function Workspace() {
function WorkspaceBody() {
const activeRequest = useAtomValue(activeRequestAtom);
const activeFolder = useAtomValue(activeFolderAtom);
const activeWorkspace = useAtomValue(activeWorkspaceAtom);
if (activeWorkspace == null) {
@@ -228,39 +232,40 @@ function WorkspaceBody() {
);
}
if (activeRequest == null) {
return (
<HotKeyList
hotkeys={['http_request.create', 'sidebar.focus', 'settings.show']}
bottomSlot={
<HStack space={1} justifyContent="center" className="mt-3">
<Button variant="border" size="sm" onClick={() => importData.mutate()}>
Import
</Button>
<CreateDropdown hideFolder>
<Button variant="border" forDropdown size="sm">
New Request
</Button>
</CreateDropdown>
</HStack>
}
/>
);
if (activeRequest?.model === 'grpc_request') {
return <GrpcConnectionLayout style={body} />;
} else if (activeRequest?.model === 'websocket_request') {
return <WebsocketRequestLayout style={body} activeRequest={activeRequest} />;
} else if (activeRequest?.model === 'http_request') {
return <HttpRequestLayout activeRequest={activeRequest} style={body} />;
} else if (activeFolder != null) {
return <FolderLayout folder={activeFolder} style={body} />;
}
if (activeRequest.model === 'grpc_request') {
return <GrpcConnectionLayout style={body} />;
} else if (activeRequest.model === 'websocket_request') {
return <WebsocketRequestLayout style={body} activeRequest={activeRequest} />;
} else {
return <HttpRequestLayout activeRequest={activeRequest} style={body} />;
}
return (
<HotKeyList
hotkeys={['model.create', 'sidebar.focus', 'settings.show']}
bottomSlot={
<HStack space={1} justifyContent="center" className="mt-3">
<Button variant="border" size="sm" onClick={() => importData.mutate()}>
Import
</Button>
<CreateDropdown hideFolder>
<Button variant="border" forDropdown size="sm">
New Request
</Button>
</CreateDropdown>
</HStack>
}
/>
);
}
function useGlobalWorkspaceHooks() {
useEnsureActiveCookieJar();
useSubscribeActiveRequestId();
useSubscribeActiveFolderId();
useSubscribeActiveEnvironmentId();
useSubscribeActiveCookieJarId();
@@ -274,7 +279,7 @@ function useGlobalWorkspaceHooks() {
const toggleCommandPalette = useToggleCommandPalette();
useHotKey('command_palette.toggle', toggleCommandPalette);
useHotKey('http_request.duplicate', () =>
duplicateRequestAndNavigate(jotaiStore.get(activeRequestAtom)),
useHotKey('model.duplicate', () =>
duplicateRequestOrFolderAndNavigate(jotaiStore.get(activeRequestAtom)),
);
}

View File

@@ -16,7 +16,7 @@ import { ImportCurlButton } from './ImportCurlButton';
import { LicenseBadge } from './LicenseBadge';
import { RecentRequestsDropdown } from './RecentRequestsDropdown';
import { SettingsDropdown } from './SettingsDropdown';
import { SidebarActions } from './sidebar/SidebarActions';
import { SidebarActions } from './SidebarActions';
import { WorkspaceActionsDropdown } from './WorkspaceActionsDropdown';
interface Props {

View File

@@ -1,6 +1,6 @@
import { useCallback, useMemo } from 'react';
import { generateId } from '../../lib/generateId';
import { Editor } from './Editor/Editor';
import { Editor } from './Editor/LazyEditor';
import type { Pair, PairEditorProps, PairWithId } from './PairEditor';
type Props = PairEditorProps;

View File

@@ -44,7 +44,7 @@ export function Confirm({
onChange={setConfirm}
label={
<>
Type <strong className="select-auto cursor-text">{requireTyping}</strong> to confirm
Type <strong className="!select-auto cursor-auto">{requireTyping}</strong> to confirm
</>
}
/>

View File

@@ -221,7 +221,7 @@ export const Dropdown = forwardRef<DropdownRef, DropdownProps>(function Dropdown
);
});
interface ContextMenuProps {
export interface ContextMenuProps {
triggerPosition: { x: number; y: number } | null;
className?: string;
items: DropdownProps['items'];
@@ -413,7 +413,7 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle' | 'items'
close: handleClose,
prev: handlePrev,
next: handleNext,
async select() {
select: async () => {
const item = items[selectedIndexRef.current ?? -1] ?? null;
if (!item) return;
await handleSelect(item);
@@ -569,10 +569,7 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle' | 'items'
<div
key={i}
className={classNames('my-1 mx-2 max-w-xs')}
onClick={() => {
// Ensure the dropdown is closed when anything in the content is clicked
onClose();
}}
onClick={onClose}
>
{item.label}
</div>

View File

@@ -19,6 +19,11 @@
@apply text-surface !important;
}
/* Matching bracket */
.cm-matchingBracket {
@apply bg-transparent border-b border-b-text-subtle;
}
&:not(.cm-focused) {
.cm-cursor, .cm-fat-cursor {
@apply hidden;
@@ -233,15 +238,15 @@
.cm-tooltip.cm-tooltip-hover {
@apply shadow-lg bg-surface rounded text-text-subtle border border-border-subtle z-50 pointer-events-auto text-sm;
@apply px-2 py-1;
@apply p-1.5;
/* Style the tooltip for popping up "open in browser" and other stuff */
a, button {
@apply text-text hover:bg-surface-highlight w-full h-sm flex items-center px-2 rounded;
}
a {
@apply text-text;
&:hover {
@apply underline;
}
@apply cursor-default !important;
&::after {
@apply text-text bg-text h-3 w-3 ml-1;
content: '';

View File

@@ -24,16 +24,20 @@ import {
useCallback,
useEffect,
useImperativeHandle,
useLayoutEffect,
useMemo,
useRef,
} from 'react';
import { activeWorkspaceAtom } from '../../../hooks/useActiveWorkspace';
import type { WrappedEnvironmentVariable } from '../../../hooks/useEnvironmentVariables';
import { useEnvironmentVariables } from '../../../hooks/useEnvironmentVariables';
import { useRandomKey } from '../../../hooks/useRandomKey';
import { useRequestEditor } from '../../../hooks/useRequestEditor';
import { useTemplateFunctionCompletionOptions } from '../../../hooks/useTemplateFunctions';
import { showDialog } from '../../../lib/dialog';
import { editEnvironment } from '../../../lib/editEnvironment';
import { tryFormatJson, tryFormatXml } from '../../../lib/formatters';
import { jotaiStore } from '../../../lib/jotai';
import { withEncryptionEnabled } from '../../../lib/setupOrConfigureEncryption';
import { TemplateFunctionDialog } from '../../TemplateFunctionDialog';
import { TemplateVariableDialog } from '../../TemplateVariableDialog';
@@ -114,7 +118,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
disabled,
extraExtensions,
forcedEnvironmentId,
forceUpdateKey,
forceUpdateKey: forceUpdateKeyFromAbove,
format,
heightMode,
hideGutter,
@@ -145,6 +149,10 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
? allEnvironmentVariables.filter(autocompleteVariables)
: allEnvironmentVariables;
}, [allEnvironmentVariables, autocompleteVariables]);
// Track a local key for updates. If the default value is changed when the input is not in focus,
// regenerate this to force the field to update.
const [focusedUpdateKey, regenerateFocusedUpdateKey] = useRandomKey('initial');
const forceUpdateKey = `${forceUpdateKeyFromAbove}::${focusedUpdateKey}`;
if (settings && wrapLines === undefined) {
wrapLines = settings.editorSoftWrap;
@@ -286,18 +294,22 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
size: 'md',
title: <InlineCode>{fn.name}()</InlineCode>,
description: fn.description,
render: ({ hide }) => (
<TemplateFunctionDialog
templateFunction={fn}
hide={hide}
initialTokens={initialTokens}
onChange={(insert) => {
cm.current?.view.dispatch({
changes: [{ from: startPos, to: startPos + tagValue.length, insert }],
});
}}
/>
),
render: ({ hide }) => {
const model = jotaiStore.get(activeWorkspaceAtom)!;
return (
<TemplateFunctionDialog
templateFunction={fn}
model={model}
hide={hide}
initialTokens={initialTokens}
onChange={(insert) => {
cm.current?.view.dispatch({
changes: [{ from: startPos, to: startPos + tagValue.length, insert }],
});
}}
/>
);
},
});
if (fn.name === 'secure') {
@@ -464,33 +476,24 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
// For read-only mode, update content when `defaultValue` changes
useEffect(
function updateReadOnlyEditor() {
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,
}),
});
if (readOnly && cm.current?.view != null) {
updateContents(cm.current.view, defaultValue || '');
}
},
[defaultValue, readOnly],
);
// Force input to update when receiving change and not in focus
useLayoutEffect(
function updateNonFocusedEditor() {
const notFocused = !cm.current?.view.hasFocus;
if (notFocused && cm.current != null) {
updateContents(cm.current.view, defaultValue || '');
}
},
[defaultValue, readOnly, regenerateFocusedUpdateKey],
);
// Add bg classes to actions, so they appear over the text
const decoratedActions = useMemo(() => {
const results = [];
@@ -697,3 +700,30 @@ function getCachedEditorState(doc: string, stateKey: string | null) {
function computeFullStateKey(stateKey: string): string {
return `editor.${stateKey}`;
}
function updateContents(view: EditorView, text: string) {
// Replace codemirror contents
const currentDoc = view.state.doc.toString();
if (currentDoc === text) {
return;
} else if (text.startsWith(currentDoc)) {
// If we're just appending, append only the changes. This preserves
// things like scroll position.
view.dispatch({
changes: view.state.changes({
from: currentDoc.length,
insert: text.slice(currentDoc.length),
}),
});
} else {
// If we're replacing everything, reset the entire content
view.dispatch({
changes: view.state.changes({
from: 0,
to: currentDoc.length,
insert: text,
}),
});
}
}

View File

@@ -0,0 +1,13 @@
import type { EditorView } from '@codemirror/view';
import { forwardRef, lazy, Suspense } from 'react';
import type { EditorProps } from './Editor';
const Editor_ = lazy(() => import('./Editor').then((m) => ({ default: m.Editor })));
export const Editor = forwardRef<EditorView, EditorProps>(function LazyEditor(props, ref) {
return (
<Suspense>
<Editor_ ref={ref} {...props} />
</Suspense>
);
});

View File

@@ -10,7 +10,7 @@ import { json } from '@codemirror/lang-json';
import { markdown } from '@codemirror/lang-markdown';
import { xml } from '@codemirror/lang-xml';
import type { LanguageSupport } from '@codemirror/language';
import {
import { bracketMatching ,
codeFolding,
foldGutter,
foldKeymap,
@@ -258,6 +258,7 @@ export const multiLineExtensions = ({ hideGutter }: { hideGutter?: boolean }) =>
indentOnInput(),
rectangularSelection(),
crosshairCursor(),
bracketMatching(),
highlightActiveLineGutter(),
keymap.of([...searchKeymap, ...foldKeymap, ...lintKeymap]),
];

View File

@@ -1,5 +1,9 @@
import type { DecorationSet, ViewUpdate } from '@codemirror/view';
import { Decoration, EditorView, hoverTooltip, MatchDecorator, ViewPlugin } from '@codemirror/view';
import { activeWorkspaceIdAtom } from '../../../../hooks/useActiveWorkspace';
import { copyToClipboard } from '../../../../lib/copy';
import { createRequestAndNavigate } from '../../../../lib/createRequestAndNavigate';
import { jotaiStore } from '../../../../lib/jotai';
const REGEX =
/(https?:\/\/([-a-zA-Z0-9@:%._+*~#=]{1,256})+(\.[a-zA-Z0-9()]{1,6})?\b([-a-zA-Z0-9()@:%_+*.~#?&/={}[\]]*))/g;
@@ -32,11 +36,38 @@ const tooltip = hoverTooltip(
pos: found.start,
end: found.end,
create() {
const dom = document.createElement('a');
dom.textContent = 'Open in browser';
dom.href = text.substring(found!.start - from, found!.end - from);
dom.target = '_blank';
dom.rel = 'noopener noreferrer';
const workspaceId = jotaiStore.get(activeWorkspaceIdAtom);
const link = text.substring(found!.start - from, found!.end - from);
const dom = document.createElement('div');
const $open = document.createElement('a');
$open.textContent = 'Open in browser';
$open.href = link;
$open.target = '_blank';
$open.rel = 'noopener noreferrer';
const $copy = document.createElement('button');
$copy.textContent = 'Copy to clipboard';
$copy.addEventListener('click', () => {
copyToClipboard(link);
});
const $create = document.createElement('button');
$create.textContent = 'Create new request';
$create.addEventListener('click', async () => {
await createRequestAndNavigate({
model: 'http_request',
workspaceId: workspaceId ?? 'n/a',
url: link,
});
});
dom.appendChild($open);
dom.appendChild($copy);
if (workspaceId != null) {
dom.appendChild($create);
}
return { dom };
},
};

View File

@@ -1,7 +1,8 @@
import { settingsAtom } from '@yaakapp-internal/models';
import type { GrpcRequest, HttpRequest, WebsocketRequest } from '@yaakapp-internal/models';
import { settingsAtom } from '@yaakapp-internal/models';
import classNames from 'classnames';
import { useAtomValue } from 'jotai';
import { memo } from 'react';
interface Props {
request: HttpRequest | GrpcRequest | WebsocketRequest;
@@ -18,42 +19,69 @@ const methodNames: Record<string, string> = {
options: 'OPTN',
head: 'HEAD',
query: 'QURY',
graphql: 'GQL',
grpc: 'GRPC',
websocket: 'WS',
};
export function HttpMethodTag({ request, className, short }: Props) {
export const HttpMethodTag = memo(function HttpMethodTag({ request, className, short }: Props) {
const settings = useAtomValue(settingsAtom);
const method =
request.model === 'http_request' && request.bodyType === 'graphql'
? 'GQL'
? 'graphql'
: request.model === 'grpc_request'
? 'GRPC'
? 'grpc'
: request.model === 'websocket_request'
? 'WS'
? 'websocket'
: request.method;
let label = method.toUpperCase();
return (
<HttpMethodTagRaw
method={method}
colored={settings.coloredMethods}
className={className}
short={short}
/>
);
});
function HttpMethodTagRaw({
className,
method,
colored,
short,
}: {
method: string;
className?: string;
colored: boolean;
short?: boolean;
}) {
let label = method.toUpperCase();
if (short) {
label = methodNames[method.toLowerCase()] ?? method.slice(0, 4);
label = label.padStart(4, ' ');
}
const m = method.toUpperCase();
return (
<span
className={classNames(
className,
!settings.coloredMethods && 'text-text-subtle',
settings.coloredMethods && method === 'GQL' && 'text-info',
settings.coloredMethods && method === 'WS' && 'text-info',
settings.coloredMethods && method === 'GRPC' && 'text-info',
settings.coloredMethods && method === 'OPTIONS' && 'text-info',
settings.coloredMethods && method === 'HEAD' && 'text-info',
settings.coloredMethods && method === 'GET' && 'text-primary',
settings.coloredMethods && method === 'PUT' && 'text-warning',
settings.coloredMethods && method === 'PATCH' && 'text-notice',
settings.coloredMethods && method === 'POST' && 'text-success',
settings.coloredMethods && method === 'DELETE' && 'text-danger',
!colored && 'text-text-subtle',
colored && m === 'GRAPHQL' && 'text-info',
colored && m === 'WEBSOCKET' && 'text-info',
colored && m === 'GRPC' && 'text-info',
colored && m === 'QUERY' && 'text-secondary',
colored && m === 'OPTIONS' && 'text-info',
colored && m === 'HEAD' && 'text-secondary',
colored && m === 'GET' && 'text-primary',
colored && m === 'PUT' && 'text-warning',
colored && m === 'PATCH' && 'text-notice',
colored && m === 'POST' && 'text-success',
colored && m === 'DELETE' && 'text-danger',
'font-mono flex-shrink-0 whitespace-pre',
'pt-[0.25em]', // Fix for monospace font not vertically centering
'pt-[0.15em]', // Fix for monospace font not vertically centering
)}
>
{label}

View File

@@ -12,6 +12,7 @@ export function HttpResponseDurationTag({ response }: Props) {
// Calculate the duration of the response for use when the response hasn't finished yet
useEffect(() => {
clearInterval(timeout.current);
if (response.state === 'closed') return;
timeout.current = setInterval(() => {
setFallbackElapsed(Date.now() - new Date(response.createdAt + 'Z').getTime());
}, 100);

View File

@@ -33,7 +33,7 @@ export function HttpStatusTag({ response, className, showReason, short }: Props)
}
return (
<span className={classNames(className, 'font-mono', colorClass)}>
<span className={classNames(className, 'font-mono min-w-0', colorClass)}>
{label} {showReason && 'statusReason' in response ? response.statusReason : null}
</span>
);

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