mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-01-15 21:53:36 +01:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb1916b773 | ||
|
|
a3df0489b1 | ||
|
|
b19e036a61 | ||
|
|
b51e37f221 | ||
|
|
cf9882b5b9 | ||
|
|
bbf85c953d | ||
|
|
17ddc76223 | ||
|
|
754ec0ba86 | ||
|
|
1198aa7d87 | ||
|
|
43437abae7 | ||
|
|
9439cfa2ba | ||
|
|
a731ccc8bd | ||
|
|
451c8b9dde | ||
|
|
b7682db9a3 |
@@ -22,7 +22,7 @@
|
||||
<!-- sponsors-premium --><a href="https://github.com/MVST-Solutions"><img src="https://github.com/MVST-Solutions.png" width="80px" alt="User avatar: MVST-Solutions" /></a> <a href="https://github.com/dharsanb"><img src="https://github.com/dharsanb.png" width="80px" alt="User avatar: dharsanb" /></a> <a href="https://github.com/railwayapp"><img src="https://github.com/railwayapp.png" width="80px" alt="User avatar: railwayapp" /></a> <a href="https://github.com/caseyamcl"><img src="https://github.com/caseyamcl.png" width="80px" alt="User avatar: caseyamcl" /></a> <a href="https://github.com/andriyor"><img src="https://github.com/andriyor.png" width="80px" alt="User avatar: andriyor" /></a> <a href="https://github.com/"><img src="https://raw.githubusercontent.com/JamesIves/github-sponsors-readme-action/dev/.github/assets/placeholder.png" width="80px" alt="User avatar: " /></a> <!-- sponsors-premium -->
|
||||
</p>
|
||||
<p align="center">
|
||||
<!-- sponsors-base --><a href="https://github.com/seanwash"><img src="https://github.com/seanwash.png" width="50px" alt="User avatar: seanwash" /></a> <a href="https://github.com/jerath"><img src="https://github.com/jerath.png" width="50px" alt="User avatar: jerath" /></a> <a href="https://github.com/itsa-sh"><img src="https://github.com/itsa-sh.png" width="50px" alt="User avatar: itsa-sh" /></a> <a href="https://github.com/dmmulroy"><img src="https://github.com/dmmulroy.png" width="50px" alt="User avatar: dmmulroy" /></a> <a href="https://github.com/timcole"><img src="https://github.com/timcole.png" width="50px" alt="User avatar: timcole" /></a> <a href="https://github.com/VLZH"><img src="https://github.com/VLZH.png" width="50px" alt="User avatar: VLZH" /></a> <a href="https://github.com/terasaka2k"><img src="https://github.com/terasaka2k.png" width="50px" alt="User avatar: terasaka2k" /></a> <a href="https://github.com/majudhu"><img src="https://github.com/majudhu.png" width="50px" alt="User avatar: majudhu" /></a> <!-- sponsors-base -->
|
||||
<!-- sponsors-base --><a href="https://github.com/seanwash"><img src="https://github.com/seanwash.png" width="50px" alt="User avatar: seanwash" /></a> <a href="https://github.com/jerath"><img src="https://github.com/jerath.png" width="50px" alt="User avatar: jerath" /></a> <a href="https://github.com/itsa-sh"><img src="https://github.com/itsa-sh.png" width="50px" alt="User avatar: itsa-sh" /></a> <a href="https://github.com/dmmulroy"><img src="https://github.com/dmmulroy.png" width="50px" alt="User avatar: dmmulroy" /></a> <a href="https://github.com/timcole"><img src="https://github.com/timcole.png" width="50px" alt="User avatar: timcole" /></a> <a href="https://github.com/VLZH"><img src="https://github.com/VLZH.png" width="50px" alt="User avatar: VLZH" /></a> <a href="https://github.com/terasaka2k"><img src="https://github.com/terasaka2k.png" width="50px" alt="User avatar: terasaka2k" /></a> <a href="https://github.com/majudhu"><img src="https://github.com/majudhu.png" width="50px" alt="User avatar: majudhu" /></a> <a href="https://github.com/axelrindle"><img src="https://github.com/axelrindle.png" width="50px" alt="User avatar: axelrindle" /></a> <!-- sponsors-base -->
|
||||
</p>
|
||||
|
||||

|
||||
|
||||
131
package-lock.json
generated
131
package-lock.json
generated
@@ -61,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",
|
||||
@@ -3112,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",
|
||||
@@ -3122,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": {
|
||||
@@ -3138,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"
|
||||
],
|
||||
@@ -3169,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"
|
||||
],
|
||||
@@ -3186,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"
|
||||
],
|
||||
@@ -3203,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"
|
||||
],
|
||||
@@ -3220,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"
|
||||
],
|
||||
@@ -3237,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"
|
||||
],
|
||||
@@ -3254,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"
|
||||
],
|
||||
@@ -3271,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"
|
||||
],
|
||||
@@ -3288,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"
|
||||
],
|
||||
@@ -3305,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"
|
||||
],
|
||||
@@ -3322,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"
|
||||
],
|
||||
@@ -18525,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",
|
||||
@@ -19064,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",
|
||||
@@ -19104,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": {
|
||||
|
||||
@@ -81,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",
|
||||
|
||||
@@ -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 {};
|
||||
}
|
||||
|
||||
@@ -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]) => ({
|
||||
|
||||
@@ -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",
|
||||
|
||||
10
src-tauri/Cargo.lock
generated
10
src-tauri/Cargo.lock
generated
@@ -1259,7 +1259,7 @@ dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2352,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",
|
||||
@@ -2368,7 +2368,7 @@ dependencies = [
|
||||
"libc",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"socket2 0.5.10",
|
||||
"socket2 0.6.1",
|
||||
"system-configuration",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
@@ -7816,6 +7816,7 @@ dependencies = [
|
||||
"cookie",
|
||||
"eventsource-client",
|
||||
"http",
|
||||
"hyper-util",
|
||||
"log",
|
||||
"md5 0.8.0",
|
||||
"mime_guess",
|
||||
@@ -7841,6 +7842,7 @@ dependencies = [
|
||||
"thiserror 2.0.17",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tower-service",
|
||||
"ts-rs",
|
||||
"uuid",
|
||||
"yaak-common",
|
||||
|
||||
@@ -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"
|
||||
|
||||
54
src-tauri/src/dns.rs
Normal file
54
src-tauri/src/dns.rs
Normal 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 scheme’s 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>),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
345
src-tauri/yaak-templates/src/format_xml.rs
Normal file
345
src-tauri/yaak-templates/src/format_xml.rs
Normal 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 "world"' width="10" height="20"/></root>"#;
|
||||
let want = r#"<root>
|
||||
<img src="x" alt='hello "world"' 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);
|
||||
}
|
||||
}
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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} />;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -238,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: '';
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
@@ -479,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} />
|
||||
@@ -523,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 }>({
|
||||
@@ -535,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;
|
||||
}
|
||||
@@ -551,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}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
|
||||
@@ -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}
|
||||
@@ -275,7 +296,7 @@ function TreeItem_<T extends { id: string }>({
|
||||
onClick={handleClick}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
disabled={editing}
|
||||
className="tree-item-inner 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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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(
|
||||
|
||||
1
src-web/modules.d.ts
vendored
1
src-web/modules.d.ts
vendored
@@ -1 +1,2 @@
|
||||
declare module 'format-graphql';
|
||||
declare module 'xml-beautify';
|
||||
|
||||
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user