Compare commits

..

25 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
59 changed files with 1418 additions and 595 deletions

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;<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;<!-- 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)

148
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",
@@ -3111,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",
@@ -3121,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": {
@@ -3137,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"
],
@@ -3168,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"
],
@@ -3185,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"
],
@@ -3202,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"
],
@@ -3219,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"
],
@@ -3236,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"
],
@@ -3253,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"
],
@@ -3270,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"
],
@@ -3287,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"
],
@@ -3304,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"
],
@@ -3321,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"
],
@@ -4189,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
@@ -17852,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": {
@@ -18520,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",
@@ -18882,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"
@@ -19055,7 +19049,7 @@
"@tanstack/react-query": "^5.90.5",
"@tanstack/react-router": "^1.133.13",
"@tanstack/react-virtual": "^3.13.12",
"@tauri-apps/api": "^2.8.0",
"@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",
@@ -19095,7 +19089,7 @@
"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": {
@@ -19119,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

@@ -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

@@ -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) {

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

@@ -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

@@ -45,6 +45,7 @@ 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;
@@ -60,6 +61,7 @@ mod updates;
mod uri_scheme;
mod window;
mod window_menu;
mod dns;
#[derive(serde::Serialize)]
#[serde(default, rename_all = "camelCase")]
@@ -738,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>,
@@ -1414,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,

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

@@ -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

@@ -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

@@ -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;

View File

@@ -503,10 +503,22 @@ impl PluginManager {
.iter()
.find(|r| r.functions.iter().any(|f| f.name == fn_name))
.ok_or_else(|| PluginNotFoundErr(fn_name.into()))?;
let plugin = self
.get_plugin_by_ref_id(&r.plugin_ref_id)
.await
.ok_or_else(|| PluginNotFoundErr(r.plugin_ref_id.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);

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

@@ -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

@@ -4,6 +4,7 @@ pub mod format;
pub mod parser;
pub mod renderer;
pub mod wasm;
pub mod format_xml;
pub use parser::*;
pub use renderer::*;

View File

@@ -85,6 +85,11 @@ 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',
@@ -177,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),
});

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

@@ -1,11 +1,14 @@
import { activeRequestAtom } from '../hooks/useActiveRequest';
import { useSubscribeActiveWorkspaceId } from '../hooks/useActiveWorkspace';
import { useActiveWorkspaceChangedToast } from '../hooks/useActiveWorkspaceChangedToast';
import { useSubscribeHotKeys } from '../hooks/useHotKey';
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();
@@ -21,5 +24,15 @@ export function GlobalHooks() {
useActiveWorkspaceChangedToast();
useSubscribeHotKeys();
useHotKey(
'request.rename',
async () => {
const model = jotaiStore.get(activeRequestAtom);
if (model == null) return;
await renameModelWithPrompt(model);
},
{ allowDefault: true },
);
return null;
}

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

@@ -1,7 +1,7 @@
import type { HttpResponse } from '@yaakapp-internal/models';
import classNames from 'classnames';
import type { CSSProperties, ReactNode} from 'react';
import React, { Suspense , lazy, 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';
@@ -30,8 +32,6 @@ import { HTMLOrTextViewer } from './responseViewers/HTMLOrTextViewer';
import { ImageViewer } from './responseViewers/ImageViewer';
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 })),
@@ -184,13 +184,13 @@ export function HttpResponsePane({ style, className, activeRequestId }: Props) {
) : mimeType?.match(/^image\/svg/) ? (
<SvgViewer response={activeResponse} />
) : mimeType?.match(/^image/i) ? (
<EnsureCompleteResponse response={activeResponse} render={ImageViewer} />
<EnsureCompleteResponse response={activeResponse} Component={ImageViewer} />
) : mimeType?.match(/^audio/i) ? (
<EnsureCompleteResponse response={activeResponse} render={AudioViewer} />
<EnsureCompleteResponse response={activeResponse} Component={AudioViewer} />
) : mimeType?.match(/^video/i) ? (
<EnsureCompleteResponse response={activeResponse} render={VideoViewer} />
<EnsureCompleteResponse response={activeResponse} Component={VideoViewer} />
) : mimeType?.match(/pdf/i) ? (
<EnsureCompleteResponse response={activeResponse} render={PdfViewer} />
<EnsureCompleteResponse response={activeResponse} Component={PdfViewer} />
) : mimeType?.match(/csv|tab-separated/i) ? (
<CsvViewer className="pb-2" response={activeResponse} />
) : (
@@ -220,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>;
@@ -238,5 +238,5 @@ function EnsureCompleteResponse({
);
}
return render({ bodyPath: response.bodyPath });
return <Component bodyPath={response.bodyPath} />;
}

View File

@@ -256,38 +256,53 @@ const sidebarTreeAtom = atom((get) => {
});
const actions = {
'sidebar.delete_selected_item': async function (items: SidebarModel[]) {
await deleteModelWithConfirm(items);
'sidebar.selected.delete': {
enable: isSidebarFocused,
cb: async function (_: TreeHandle, items: SidebarModel[]) {
await deleteModelWithConfirm(items);
},
},
'model.duplicate': async function (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));
}
'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);
}
},
},
'request.send': async function (items: SidebarModel[]) {
await Promise.all(
items.filter((i) => i.model === 'http_request').map((i) => sendAnyHttpRequest.mutate(i.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'] = {
priority: 10, // So these ones take precedence over global hotkeys when the sidebar is focused
actions,
enable: () => isSidebarFocused(),
};
const hotkeys: TreeProps<SidebarModel>['hotkeys'] = { actions };
async function getContextMenu(items: SidebarModel[]): Promise<DropdownItem[]> {
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) {
console.log('HELLO', child);
return getCreateDropdownItems({ workspaceId, activeRequest: null, folderId: null });
}
@@ -321,7 +336,7 @@ async function getContextMenu(items: SidebarModel[]): Promise<DropdownItem[]> {
hotKeyLabelOnly: true,
hidden: !onlyHttpRequests,
leftSlot: <Icon icon="send_horizontal" />,
onSelect: () => actions['request.send'](items),
onSelect: () => actions['request.send'].cb(tree, items),
},
...(items.length === 1 && child.model === 'http_request'
? await getHttpRequestActions()
@@ -362,6 +377,8 @@ async function getContextMenu(items: SidebarModel[]): Promise<DropdownItem[]> {
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'],
@@ -375,7 +392,7 @@ async function getContextMenu(items: SidebarModel[]): Promise<DropdownItem[]> {
hotKeyAction: 'model.duplicate',
hotKeyLabelOnly: true, // Would trigger for every request (bad)
leftSlot: <Icon icon="copy" />,
onSelect: () => actions['model.duplicate'](items),
onSelect: () => actions['sidebar.selected.duplicate'].cb(tree, items),
},
{
label: 'Move',
@@ -393,10 +410,10 @@ async function getContextMenu(items: SidebarModel[]): Promise<DropdownItem[]> {
{
color: 'danger',
label: 'Delete',
hotKeyAction: 'sidebar.delete_selected_item',
hotKeyAction: 'sidebar.selected.delete',
hotKeyLabelOnly: true,
leftSlot: <Icon icon="trash" />,
onSelect: () => actions['sidebar.delete_selected_item'](items),
onSelect: () => actions['sidebar.selected.delete'].cb(tree, items),
},
...modelCreationItems,
];

View File

@@ -1,11 +1,10 @@
import classNames from 'classnames';
import { FocusTrap } from 'focus-trap-react';
import * as m from 'motion/react-m';
import type { ReactNode} from 'react';
import React, { Suspense , lazy, useRef } from 'react';
import type { ReactNode } from 'react';
import React, { useRef } from 'react';
import { Portal } from './Portal';
const FocusTrap = lazy(() => import('focus-trap-react'));
interface Props {
children: ReactNode;
portalName: string;
@@ -51,52 +50,50 @@ export function Overlay({
return (
<Portal name={portalName}>
{open && (
<Suspense>
<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,
}}
<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 }}
>
<m.div
ref={containerRef}
tabIndex={-1}
className={classNames('fixed inset-0', zIndexes[zIndex])}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
>
<div
aria-hidden
onClick={onClose}
className={classNames(
'absolute inset-0',
variant === 'default' && 'bg-backdrop backdrop-blur-sm',
)}
/>
{/* Show the draggable region at the top */}
{/* TODO: Figure out tauri drag region and also make clickable still */}
{variant === 'default' && (
<div data-tauri-drag-region className="absolute top-0 left-0 h-md right-0" />
<div
aria-hidden
onClick={onClose}
className={classNames(
'absolute inset-0',
variant === 'default' && 'bg-backdrop backdrop-blur-sm',
)}
{children}
</m.div>
</FocusTrap>
</Suspense>
/>
{/* Show the draggable region at the top */}
{/* TODO: Figure out tauri drag region and also make clickable still */}
{variant === 'default' && (
<div data-tauri-drag-region className="absolute top-0 left-0 h-md right-0" />
)}
{children}
</m.div>
</FocusTrap>
)}
</Portal>
);

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

@@ -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,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

@@ -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

@@ -151,7 +151,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
}, [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();
const [focusedUpdateKey, regenerateFocusedUpdateKey] = useRandomKey('initial');
const forceUpdateKey = `${forceUpdateKeyFromAbove}::${focusedUpdateKey}`;
if (settings && wrapLines === undefined) {
@@ -352,17 +352,6 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
[],
);
// Force input to update when receiving change and not in focus
useLayoutEffect(() => {
const currDoc = cm.current?.view.state.doc.toString() || '';
const nextDoc = defaultValue || '';
const notFocused = !cm.current?.view.hasFocus;
const hasChanged = currDoc !== nextDoc;
if (notFocused && hasChanged) {
regenerateFocusedUpdateKey();
}
}, [defaultValue, regenerateFocusedUpdateKey]);
const [, { focusParamValue }] = useRequestEditor();
const onClickPathParameter = useCallback(
async (name: string) => {
@@ -487,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 = [];
@@ -720,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

@@ -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,17 +1,8 @@
import classNames from 'classnames';
import type {
CSSProperties,
KeyboardEvent,
ReactNode} from 'react';
import React, {
lazy,
Suspense,
useRef,
useState,
} from 'react';
import type { CSSProperties, KeyboardEvent, ReactNode } from 'react';
import React, { useRef, useState } from 'react';
import { generateId } from '../../lib/generateId';
const Portal = lazy(() => import('../Portal').then((m) => ({ default: m.Portal })));
import { Portal } from '../Portal';
export interface TooltipProps {
children: ReactNode;
@@ -75,7 +66,7 @@ export function Tooltip({ children, content, tabIndex, size = 'md' }: TooltipPro
const id = useRef(`tooltip-${generateId()}`);
return (
<Suspense>
<>
<Portal name="tooltip">
<div
ref={tooltipRef}
@@ -114,7 +105,7 @@ export function Tooltip({ children, content, tabIndex, size = 'md' }: TooltipPro
>
{children}
</span>
</Suspense>
</>
);
}

View File

@@ -36,7 +36,7 @@ import {
import type { SelectableTreeNode, TreeNode } from './common';
import { equalSubtree, getSelectedItems, hasAncestor } from './common';
import { TreeDragOverlay } from './TreeDragOverlay';
import type { TreeItemProps } from './TreeItem';
import type { TreeItemHandle, TreeItemProps } from './TreeItem';
import type { TreeItemListProps } from './TreeItemList';
import { TreeItemList } from './TreeItemList';
import { useSelectableItems } from './useSelectableItems';
@@ -45,13 +45,23 @@ export interface TreeProps<T extends { id: string }> {
root: TreeNode<T>;
treeId: string;
getItemKey: (item: T) => string;
getContextMenu?: (items: T[]) => Promise<ContextMenuProps['items']>;
getContextMenu?: (t: TreeHandle, items: T[]) => Promise<ContextMenuProps['items']>;
ItemInner: ComponentType<{ treeId: string; item: T }>;
ItemLeftSlot?: ComponentType<{ treeId: string; item: T }>;
className?: string;
onActivate?: (item: T) => void;
onDragEnd?: (opt: { items: T[]; parent: T; children: T[]; insertAt: number }) => void;
hotkeys?: { actions: Partial<Record<HotkeyAction, (items: T[]) => void>> } & HotKeyOptions;
hotkeys?: {
actions: Partial<
Record<
HotkeyAction,
{
cb: (h: TreeHandle, items: T[]) => void;
enable?: boolean | ((h: TreeHandle) => boolean);
} & Omit<HotKeyOptions, 'enable'>
>
>;
};
getEditOptions?: (item: T) => {
defaultValue: string;
placeholder?: string;
@@ -62,6 +72,7 @@ export interface TreeProps<T extends { id: string }> {
export interface TreeHandle {
focus: () => void;
selectItem: (id: string) => void;
renameItem: (id: string) => void;
}
function TreeInner<T extends { id: string }>(
@@ -87,6 +98,15 @@ function TreeInner<T extends { id: string }>(
x: number;
y: number;
} | null>(null);
const treeItemRefs = useRef<Record<string, TreeItemHandle>>({});
const handleAddTreeItemRef = useCallback((item: T, r: TreeItemHandle | null) => {
if (r == null) {
delete treeItemRefs.current[item.id];
} else {
treeItemRefs.current[item.id] = r;
}
}, []);
const handleCloseContextMenu = useCallback(() => {
setShowContextMenu(null);
@@ -105,11 +125,11 @@ function TreeInner<T extends { id: string }>(
[treeId, tryFocus],
);
useImperativeHandle(
ref,
(): TreeHandle => ({
const treeHandle = useMemo<TreeHandle>(
() => ({
focus: tryFocus,
selectItem(id) {
renameItem: (id) => treeItemRefs.current[id]?.rename(),
selectItem: (id) => {
setSelected([id], false);
jotaiStore.set(focusIdsFamily(treeId), { anchorId: id, lastId: id });
},
@@ -117,6 +137,8 @@ function TreeInner<T extends { id: string }>(
[setSelected, treeId, tryFocus],
);
useImperativeHandle(ref, (): TreeHandle => treeHandle, [treeHandle]);
const handleGetContextMenu = useMemo(() => {
if (getContextMenu == null) return;
return (item: T) => {
@@ -124,16 +146,16 @@ function TreeInner<T extends { id: string }>(
const isSelected = items.find((i) => i.id === item.id);
if (isSelected) {
// If right-clicked an item that was in the multiple-selection, use the entire selection
return getContextMenu(items);
return getContextMenu(treeHandle, items);
} else {
// If right-clicked an item that was NOT in the multiple-selection, just use that one
// Also update the selection with it
jotaiStore.set(selectedIdsFamily(treeId), [item.id]);
jotaiStore.set(focusIdsFamily(treeId), (prev) => ({ ...prev, lastId: item.id }));
return getContextMenu([item]);
return getContextMenu(treeHandle, [item]);
}
};
}, [getContextMenu, selectableItems, treeId]);
}, [getContextMenu, selectableItems, treeHandle, treeId]);
const handleSelect = useCallback<NonNullable<TreeItemProps<T>['onClick']>>(
(item, { shiftKey, metaKey, ctrlKey }) => {
@@ -141,7 +163,7 @@ function TreeInner<T extends { id: string }>(
const selectedIdsAtom = selectedIdsFamily(treeId);
const selectedIds = jotaiStore.get(selectedIdsAtom);
// Mark item as the last one selected
// Mark the item as the last one selected
jotaiStore.set(focusIdsFamily(treeId), (prev) => ({ ...prev, lastId: item.id }));
if (shiftKey) {
@@ -427,17 +449,22 @@ function TreeInner<T extends { id: string }>(
e.preventDefault();
e.stopPropagation();
const items = await getContextMenu([]);
const items = await getContextMenu(treeHandle, []);
setShowContextMenu({ items, x: e.clientX, y: e.clientY });
},
[getContextMenu],
[getContextMenu, treeHandle],
);
const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 6 } }));
return (
<>
<TreeHotKeys treeId={treeId} hotkeys={hotkeys} selectableItems={selectableItems} />
<TreeHotKeys
treeHandle={treeHandle}
treeId={treeId}
hotkeys={hotkeys}
selectableItems={selectableItems}
/>
{showContextMenu && (
<ContextMenu
items={showContextMenu.items}
@@ -466,6 +493,7 @@ function TreeInner<T extends { id: string }>(
>
<div
className={classNames(
'[&_.tree-item.selected_.tree-item-inner]:text-text',
'[&:focus-within]:[&_.tree-item.selected]:bg-surface-active',
'[&:not(:focus-within)]:[&_.tree-item.selected]:bg-surface-highlight',
@@ -478,7 +506,12 @@ function TreeInner<T extends { id: string }>(
'[&_.tree-item.selected:has(+.drop-marker+.tree-item.selected)]:rounded-b-none',
)}
>
<TreeItemList nodes={selectableItems} treeId={treeId} {...treeItemListProps} />
<TreeItemList
addTreeItemRef={handleAddTreeItemRef}
nodes={selectableItems}
treeId={treeId}
{...treeItemListProps}
/>
</div>
{/* Assign root ID so we can reuse our same move/end logic */}
<DropRegionAfterList id={root.item.id} onContextMenu={handleContextMenu} />
@@ -522,11 +555,14 @@ function DropRegionAfterList({
return <div ref={setNodeRef} onContextMenu={onContextMenu} />;
}
interface TreeHotKeyProps<T extends { id: string }> extends HotKeyOptions {
interface TreeHotKeyProps<T extends { id: string }> {
action: HotkeyAction;
selectableItems: SelectableTreeNode<T>[];
treeId: string;
onDone: (items: T[]) => void;
onDone: (h: TreeHandle, items: T[]) => void;
treeHandle: TreeHandle;
priority?: number;
enable?: boolean | ((h: TreeHandle) => boolean);
}
function TreeHotKey<T extends { id: string }>({
@@ -534,14 +570,23 @@ function TreeHotKey<T extends { id: string }>({
action,
onDone,
selectableItems,
treeHandle,
enable,
...options
}: TreeHotKeyProps<T>) {
useHotKey(
action,
() => {
onDone(getSelectedItems(treeId, selectableItems));
onDone(treeHandle, getSelectedItems(treeId, selectableItems));
},
{
...options,
enable: () => {
if (enable == null) return true;
if (typeof enable === 'function') return enable(treeHandle);
else return enable;
},
},
options,
);
return null;
}
@@ -550,24 +595,26 @@ function TreeHotKeys<T extends { id: string }>({
treeId,
hotkeys,
selectableItems,
treeHandle,
}: {
treeId: string;
hotkeys: TreeProps<T>['hotkeys'];
selectableItems: SelectableTreeNode<T>[];
treeHandle: TreeHandle;
}) {
if (hotkeys == null) return null;
return (
<>
{Object.entries(hotkeys.actions).map(([hotkey, onDone]) => (
{Object.entries(hotkeys.actions).map(([hotkey, { cb, ...options }]) => (
<TreeHotKey
key={hotkey}
action={hotkey as HotkeyAction}
priority={hotkeys.priority}
enable={hotkeys.enable}
treeId={treeId}
onDone={onDone}
onDone={cb}
treeHandle={treeHandle}
selectableItems={selectableItems}
{...options}
/>
))}
</>

View File

@@ -1,16 +1,19 @@
import classNames from 'classnames';
import { useAtomValue } from 'jotai';
import { memo } from 'react';
import { hoveredParentDepthFamily } from './atoms';
import { hoveredParentDepthFamily, isParentHoveredFamily } from './atoms';
export const TreeIndentGuide = memo(function TreeIndentGuide({
treeId,
depth,
parentId,
}: {
treeId: string;
depth: number;
parentId: string | null;
}) {
const parentDepth = useAtomValue(hoveredParentDepthFamily(treeId));
const isHovered = useAtomValue(isParentHoveredFamily({ treeId, parentId }));
return (
<div className="flex">
@@ -19,7 +22,7 @@ export const TreeIndentGuide = memo(function TreeIndentGuide({
key={i}
className={classNames(
'w-[1rem] border-r border-r-text-subtlest',
parentDepth !== i + 1 && 'opacity-30',
!(parentDepth === i + 1 && isHovered) && 'opacity-30',
)}
/>
))}

View File

@@ -30,8 +30,14 @@ export type TreeItemProps<T extends { id: string }> = Pick<
onClick?: (item: T, e: OnClickEvent) => void;
getContextMenu?: (item: T) => Promise<ContextMenuProps['items']>;
depth: number;
addRef?: (item: T, n: TreeItemHandle | null) => void;
};
export interface TreeItemHandle {
rename: () => void;
isRenaming: boolean;
}
const HOVER_CLOSED_FOLDER_DELAY = 800;
function TreeItem_<T extends { id: string }>({
@@ -44,8 +50,9 @@ function TreeItem_<T extends { id: string }>({
getEditOptions,
className,
depth,
addRef,
}: TreeItemProps<T>) {
const ref = useRef<HTMLLIElement>(null);
const listItemRef = useRef<HTMLLIElement>(null);
const draggableRef = useRef<HTMLButtonElement>(null);
const isSelected = useAtomValue(isSelectedFamily({ treeId, itemId: node.item.id }));
const isCollapsed = useAtomValue(isCollapsedFamily({ treeId, itemId: node.item.id }));
@@ -54,6 +61,17 @@ function TreeItem_<T extends { id: string }>({
const [dropHover, setDropHover] = useState<null | 'drop' | 'animate'>(null);
const startedHoverTimeout = useRef<NodeJS.Timeout>(undefined);
useEffect(() => {
addRef?.(node.item, {
rename: () => {
if (getEditOptions != null) {
setEditing(true);
}
},
isRenaming: editing,
});
}, [addRef, editing, getEditOptions, node.item]);
const isAncestorCollapsedAtom = useMemo(
() =>
selectAtom(
@@ -80,7 +98,7 @@ function TreeItem_<T extends { id: string }>({
useEffect(
function scrollIntoViewWhenSelected() {
return jotaiStore.sub(isSelectedFamily({ treeId, itemId: node.item.id }), () => {
ref.current?.scrollIntoView({ block: 'nearest' });
listItemRef.current?.scrollIntoView({ block: 'nearest' });
});
},
[node.item.id, treeId],
@@ -103,10 +121,11 @@ function TreeItem_<T extends { id: string }>({
const handleSubmitNameEdit = useCallback(
async function submitNameEdit(el: HTMLInputElement) {
getEditOptions?.(node.item).onChange(node.item, el.value);
onClick?.(node.item, { shiftKey: false, ctrlKey: false, metaKey: false });
// Slight delay for the model to propagate to the local store
setTimeout(() => setEditing(false), 200);
},
[getEditOptions, node.item],
[getEditOptions, node.item, onClick],
);
const handleEditFocus = useCallback(function handleEditFocus(el: HTMLInputElement | null) {
@@ -126,8 +145,10 @@ function TreeItem_<T extends { id: string }>({
e.stopPropagation();
switch (e.key) {
case 'Enter':
e.preventDefault();
await handleSubmitNameEdit(e.currentTarget);
if (editing) {
e.preventDefault();
await handleSubmitNameEdit(e.currentTarget);
}
break;
case 'Escape':
e.preventDefault();
@@ -135,7 +156,7 @@ function TreeItem_<T extends { id: string }>({
break;
}
},
[handleSubmitNameEdit],
[editing, handleSubmitNameEdit],
);
const handleDoubleClick = useCallback(() => {
@@ -222,7 +243,7 @@ function TreeItem_<T extends { id: string }>({
return (
<li
ref={ref}
ref={listItemRef}
role="treeitem"
aria-level={depth + 1}
aria-expanded={node.children == null ? undefined : !isCollapsed}
@@ -239,7 +260,7 @@ function TreeItem_<T extends { id: string }>({
isSelected && 'selected',
)}
>
<TreeIndentGuide treeId={treeId} depth={depth} />
<TreeIndentGuide treeId={treeId} depth={depth} parentId={node.parent?.item.id ?? null} />
<div
className={classNames(
'text-text-subtle',
@@ -275,7 +296,7 @@ function TreeItem_<T extends { id: string }>({
onClick={handleClick}
onDoubleClick={handleDoubleClick}
disabled={editing}
className="px-2 focus:outline-none flex items-center gap-2 h-full whitespace-nowrap"
className="cursor-default tree-item-inner px-2 focus:outline-none flex items-center gap-2 h-full whitespace-nowrap"
{...attributes}
{...listeners}
tabIndex={isLastSelected ? 0 : -1}

View File

@@ -3,7 +3,7 @@ import { Fragment, memo } from 'react';
import type { SelectableTreeNode } from './common';
import type { TreeProps } from './Tree';
import { TreeDropMarker } from './TreeDropMarker';
import type { TreeItemProps } from './TreeItem';
import type { TreeItemHandle, TreeItemProps } from './TreeItem';
import { TreeItem } from './TreeItem';
export type TreeItemListProps<T extends { id: string }> = Pick<
@@ -15,6 +15,7 @@ export type TreeItemListProps<T extends { id: string }> = Pick<
style?: CSSProperties;
className?: string;
forceDepth?: number;
addTreeItemRef?: (item: T, n: TreeItemHandle | null) => void;
};
function TreeItemList_<T extends { id: string }>({
@@ -29,6 +30,7 @@ function TreeItemList_<T extends { id: string }>({
style,
treeId,
forceDepth,
addTreeItemRef,
}: TreeItemListProps<T>) {
return (
<ul role="tree" style={style} className={className}>
@@ -36,6 +38,7 @@ function TreeItemList_<T extends { id: string }>({
{nodes.map((child, i) => (
<Fragment key={getItemKey(child.node.item)}>
<TreeItem
addRef={addTreeItemRef}
treeId={treeId}
node={child.node}
ItemInner={ItemInner}
@@ -46,7 +49,7 @@ function TreeItemList_<T extends { id: string }>({
getItemKey={getItemKey}
depth={forceDepth == null ? child.depth : forceDepth}
/>
<TreeDropMarker node={child.node} treeId={treeId} index={i+1} />
<TreeDropMarker node={child.node} treeId={treeId} index={i + 1} />
</Fragment>
))}
</ul>

View File

@@ -45,8 +45,14 @@ export const hoveredParentFamily = atomFamily((_treeId: string) => {
});
});
export const isParentHoveredFamily = atomFamily(
({ treeId, parentId }: { treeId: string; parentId: string | null }) =>
selectAtom(hoveredParentFamily(treeId), (v) => v.parentId === parentId, Object.is),
(a, b) => a.treeId === b.treeId && a.parentId === b.parentId,
);
export const isIndexHoveredFamily = atomFamily(
({ treeId, index }: { treeId: string; index: number}) =>
({ treeId, index }: { treeId: string; index: number }) =>
selectAtom(hoveredParentFamily(treeId), (v) => v.index === index, Object.is),
(a, b) => a.treeId === b.treeId && a.index === b.index,
);
@@ -55,8 +61,8 @@ export const hoveredParentDepthFamily = atomFamily((treeId: string) =>
selectAtom(
hoveredParentFamily(treeId),
(s) => s.parentDepth,
(a, b) => Object.is(a, b) // prevents re-render unless the value changes
)
(a, b) => Object.is(a, b), // prevents re-render unless the value changes
),
);
export const collapsedFamily = atomFamily((workspaceId: string) => {

View File

@@ -3,12 +3,10 @@ import 'react-pdf/dist/Page/AnnotationLayer.css';
import { convertFileSrc } from '@tauri-apps/api/core';
import './PdfViewer.css';
import type { PDFDocumentProxy } from 'pdfjs-dist';
import React, { lazy, useRef, useState } from 'react';
import React, { useRef, useState } from 'react';
import { Document, Page } from 'react-pdf';
import { useContainerSize } from '../../hooks/useContainerQuery';
const Document = lazy(() => import('react-pdf').then((m) => ({ default: m.Document })));
const Page = lazy(() => import('react-pdf').then((m) => ({ default: m.Page })));
import('react-pdf').then(({ pdfjs }) => {
pdfjs.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.mjs',

View File

@@ -132,7 +132,7 @@ export function TextViewer({ language, text, response, requestId, pretty, classN
language={language}
actions={actions}
extraExtensions={extraExtensions}
stateKey={null}
stateKey={'response.body.' + response.id}
/>
);
}

View File

@@ -6,6 +6,7 @@ import { capitalize } from '../lib/capitalize';
import { jotaiStore } from '../lib/jotai';
const HOLD_KEYS = ['Shift', 'Control', 'Command', 'Alt', 'Meta'];
const SINGLE_WHITELIST = ['Delete', 'Enter', 'Backspace'];
export type HotkeyAction =
| 'app.zoom_in'
@@ -17,11 +18,14 @@ export type HotkeyAction =
| 'model.create'
| 'model.duplicate'
| 'request.send'
| 'request_switcher.next'
| 'request_switcher.prev'
| 'request_switcher.toggle'
| 'request.rename'
| 'switcher.next'
| 'switcher.prev'
| 'switcher.toggle'
| 'settings.show'
| 'sidebar.delete_selected_item'
| 'sidebar.selected.delete'
| 'sidebar.selected.duplicate'
| 'sidebar.selected.rename'
| 'sidebar.focus'
| 'url_bar.focus'
| 'workspace_settings.show';
@@ -32,15 +36,18 @@ const hotkeys: Record<HotkeyAction, string[]> = {
'app.zoom_reset': ['CmdCtrl+0'],
'command_palette.toggle': ['CmdCtrl+k'],
'environmentEditor.toggle': ['CmdCtrl+Shift+E', 'CmdCtrl+Shift+e'],
'request.rename': type() === 'macos' ? ['Control+Shift+r'] : ['F2'],
'request.send': ['CmdCtrl+Enter', 'CmdCtrl+r'],
'hotkeys.showHelp': ['CmdCtrl+Shift+/', 'CmdCtrl+Shift+?'], // when shift is pressed, it might be a question mark
'model.create': ['CmdCtrl+n'],
'model.duplicate': ['CmdCtrl+d'],
'request_switcher.next': ['Control+Shift+Tab'],
'request_switcher.prev': ['Control+Tab'],
'request_switcher.toggle': ['CmdCtrl+p'],
'switcher.next': ['Control+Shift+Tab'],
'switcher.prev': ['Control+Tab'],
'switcher.toggle': ['CmdCtrl+p'],
'settings.show': ['CmdCtrl+,'],
'sidebar.delete_selected_item': ['Delete', 'CmdCtrl+Backspace'],
'sidebar.selected.delete': ['Delete', 'CmdCtrl+Backspace'],
'sidebar.selected.duplicate': ['CmdCtrl+d'],
'sidebar.selected.rename': ['Enter'],
'sidebar.focus': ['CmdCtrl+b'],
'url_bar.focus': ['CmdCtrl+l'],
'workspace_settings.show': ['CmdCtrl+;'],
@@ -55,12 +62,15 @@ const hotkeyLabels: Record<HotkeyAction, string> = {
'hotkeys.showHelp': 'Show Keyboard Shortcuts',
'model.create': 'New Request',
'model.duplicate': 'Duplicate Request',
'request.rename': 'Rename',
'request.send': 'Send',
'request_switcher.next': 'Go To Previous Request',
'request_switcher.prev': 'Go To Next Request',
'request_switcher.toggle': 'Toggle Request Switcher',
'switcher.next': 'Go To Previous Request',
'switcher.prev': 'Go To Next Request',
'switcher.toggle': 'Toggle Request Switcher',
'settings.show': 'Open Settings',
'sidebar.delete_selected_item': 'Delete Request',
'sidebar.selected.delete': 'Delete',
'sidebar.selected.duplicate': 'Duplicate',
'sidebar.selected.rename': 'Rename',
'sidebar.focus': 'Focus or Toggle Sidebar',
'url_bar.focus': 'Focus URL',
'workspace_settings.show': 'Open Workspace Settings',
@@ -73,6 +83,7 @@ export const hotkeyActions: HotkeyAction[] = Object.keys(hotkeys) as (keyof type
export type HotKeyOptions = {
enable?: boolean | (() => boolean);
priority?: number;
allowDefault?: boolean;
};
interface Callback {
@@ -142,7 +153,7 @@ function handleKeyUp(e: KeyboardEvent) {
function handleKeyDown(e: KeyboardEvent) {
// Don't add key if not holding modifier
const isValidKeymapKey =
e.altKey || e.ctrlKey || e.metaKey || e.shiftKey || e.key === 'Backspace' || e.key === 'Delete';
e.altKey || e.ctrlKey || e.metaKey || e.shiftKey || SINGLE_WHITELIST.includes(e.key);
if (!isValidKeymapKey) {
return;
}
@@ -162,7 +173,7 @@ function handleKeyDown(e: KeyboardEvent) {
if (e.metaKey) currentKeysWithModifiers.add('Meta');
if (e.shiftKey) currentKeysWithModifiers.add('Shift');
for (const [hkAction, hkKeys] of Object.entries(hotkeys) as [HotkeyAction, string[]][]) {
outer: for (const [hkAction, hkKeys] of Object.entries(hotkeys) as [HotkeyAction, string[]][]) {
if (
(e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) &&
currentKeysWithModifiers.size === 1 &&
@@ -175,24 +186,24 @@ function handleKeyDown(e: KeyboardEvent) {
const executed: string[] = [];
for (const { action, callback, options } of jotaiStore.get(sortedCallbacksAtom)) {
const enable = typeof options.enable === 'function' ? options.enable() : options.enable;
if (enable === false) {
if (hkAction !== action) {
continue;
}
if (hkAction !== action) {
const enable = typeof options.enable === 'function' ? options.enable() : options.enable;
if (enable === false) {
continue;
}
for (const hkKey of hkKeys) {
const keys = hkKey.split('+').map(resolveHotkeyKey);
if (
keys.length === currentKeysWithModifiers.size &&
keys.every((key) => currentKeysWithModifiers.has(key))
) {
e.preventDefault();
e.stopPropagation();
if (compareKeys(keys, Array.from(currentKeysWithModifiers))) {
if (!options.allowDefault) {
e.preventDefault();
e.stopPropagation();
}
callback(e);
executed.push(`${action} ${options.priority ?? 0}`);
break outer;
}
}
}
@@ -258,3 +269,10 @@ const resolveHotkeyKey = (key: string) => {
else if (key === 'CmdCtrl') return 'Control';
else return key;
};
function compareKeys(keysA: string[], keysB: string[]) {
if (keysA.length !== keysB.length) return false;
const sortedA = keysA.map((k) => k.toLowerCase()).sort().join('::');
const sortedB = keysB.map((k) => k.toLowerCase()).sort().join('::');
return sortedA === sortedB;
}

View File

@@ -1,8 +1,8 @@
import { useCallback, useState } from 'react';
import { generateId } from '../lib/generateId';
export function useRandomKey() {
const [value, setValue] = useState<string>(generateId());
export function useRandomKey(initialValue?: string) {
const [value, setValue] = useState<string>(initialValue ?? generateId());
const regenerate = useCallback(() => setValue(generateId()), []);
return [value, regenerate] as const;
}

View File

@@ -10,6 +10,7 @@ export function useResponseBodyText({
filter: string | null;
}) {
return useQuery({
placeholderData: (prev) => prev, // Keep previous data on refetch
queryKey: [
'response_body_text',
response.id,

View File

@@ -1,8 +1,5 @@
import xmlFormat from 'xml-formatter';
import { invokeCmd } from './tauri';
const INDENT = ' ';
export async function tryFormatJson(text: string): Promise<string> {
if (text === '') return text;
@@ -11,14 +8,12 @@ export async function tryFormatJson(text: string): Promise<string> {
return result;
} catch (err) {
console.warn('Failed to format JSON', err);
// Nothing
}
try {
return JSON.stringify(JSON.parse(text), null, 2);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (err) {
// Nothing
console.log('JSON beautify failed', err);
}
return text;
@@ -28,9 +23,11 @@ export async function tryFormatXml(text: string): Promise<string> {
if (text === '') return text;
try {
return xmlFormat(text, { throwOnFailure: true, strictMode: false, indentation: INDENT });
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const result = await invokeCmd<string>('cmd_format_xml', { text });
return result;
} catch (err) {
return text;
console.warn('Failed to format XML', err);
}
return text;
}

View File

@@ -67,7 +67,9 @@ async function performImport(filePath: string): Promise<boolean> {
return (
<VStack space={3} className="pb-4">
<ul className="list-disc pl-6">
<li>{pluralizeCount('Workspace', imported.workspaces.length)}</li>
{imported.workspaces.length > 0 && (
<li>{pluralizeCount('Workspace', imported.workspaces.length)}</li>
)}
{imported.environments.length > 0 && (
<li>{pluralizeCount('Environment', imported.environments.length)}</li>
)}

View File

@@ -15,6 +15,7 @@ type TauriCmd =
| 'cmd_dismiss_notification'
| 'cmd_export_data'
| 'cmd_format_json'
| 'cmd_format_xml'
| 'cmd_get_http_authentication_config'
| 'cmd_get_http_authentication_summaries'
| 'cmd_get_sse_events'

View File

@@ -6,7 +6,9 @@ import { resolveAppearance } from './appearance';
export async function getThemes() {
const themes = (await invokeCmd<GetThemesResponse[]>('cmd_get_themes')).flatMap((t) => t.themes);
themes.sort((a, b) => a.label.localeCompare(b.label));
return { themes: [yaakDark, yaakLight, ...themes] };
// Remove duplicates, in case multiple plugins provide the same theme
const uniqueThemes = Array.from(new Map(themes.map((t) => [t.id, t])).values());
return { themes: [yaakDark, yaakLight, ...uniqueThemes] };
}
export async function getResolvedTheme(

View File

@@ -1 +1,2 @@
declare module 'format-graphql';
declare module 'xml-beautify';

View File

@@ -26,7 +26,7 @@
"@tanstack/react-query": "^5.90.5",
"@tanstack/react-router": "^1.133.13",
"@tanstack/react-virtual": "^3.13.12",
"@tauri-apps/api": "^2.8.0",
"@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",
@@ -66,7 +66,7 @@
"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": {
@@ -90,7 +90,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",