Compare commits

...

27 Commits

Author SHA1 Message Date
Gregory Schier
1e90e672d2 Update workflow 2024-09-06 11:09:03 -07:00
Gregory Schier
d73b692edc Fix lint issues 2024-09-06 10:59:14 -07:00
Gregory Schier
b3adbc1860 Dynamic plugins (#68) 2024-09-06 10:43:25 -07:00
Gregory Schier
f7949c9909 Don't introspect schema on interval 2024-09-06 10:37:10 -07:00
Gregory Schier
baa4577601 Bump plugin-runtime-types version 2024-09-06 06:32:35 -07:00
Gregory Schier
db935f0130 re-gen plugin-runtime-types 2024-09-06 06:26:05 -07:00
Gregory Schier
d47de9a999 Remove prints 2024-09-06 06:21:21 -07:00
Gregory Schier
82252f920f Fix import order for nested folders to prevent foreign key constraint error 2024-09-06 06:20:27 -07:00
Gregory Schier
fedf356576 Support non-utf8 charsets 2024-09-05 13:58:06 -07:00
Gregory Schier
3601410fb8 Fix banner overflow 2024-09-04 07:33:21 -07:00
Gregory Schier
f67cecf1b4 Fix interface zoom 2024-09-03 14:37:44 -07:00
Gregory Schier
8619c66ea4 Create request/folder with proper folderId 2024-09-03 08:06:30 -07:00
Gregory Schier
230e1f55c2 Separate active tabs per request 2024-09-03 07:52:35 -07:00
Gregory Schier
d4ab8897e2 Separate active tabs per request 2024-09-03 07:33:48 -07:00
Gregory Schier
184feaa22b Print plugin dir on failure to read 2024-09-03 07:18:00 -07:00
Gregory Schier
428aaad877 Clean up 2024-09-03 06:44:51 -07:00
Gregory Schier
33f1aa29e4 Request pane context (#70) 2024-09-03 06:18:25 -07:00
Gregory Schier
002acd05ee Request pane context (#69) 2024-09-02 14:36:55 -07:00
Gregory Schier
e6d7f4a928 Fix white screen 2024-09-02 12:52:42 -07:00
Gregory Schier
0bfafb284a Placeholder CM tags working 2024-09-02 12:35:05 -07:00
Gregory Schier
f8b317e94b Rename var 2024-08-30 05:39:58 -07:00
Gregory Schier
ef0fdb4b16 Check for updates less often on stable 2024-08-30 05:39:29 -07:00
Gregory Schier
f2f1d9affa Only check for updates once per day 2024-08-30 05:36:30 -07:00
Gregory Schier
c73262b037 URL path placeholders 2024-08-30 05:24:07 -07:00
Gregory Schier
f8936e7b76 Detect JSON APIs returning HTML content-type 2024-08-29 10:52:41 -07:00
Gregory Schier
0d20e0fe29 Better content-type detection for editor 2024-08-29 06:07:31 -07:00
Gregory Schier
ba626a6b3e Fix Windows/linux close icon color 2024-08-28 09:46:35 -07:00
92 changed files with 1696 additions and 690 deletions

View File

@@ -81,7 +81,7 @@ jobs:
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Install yaak CLI
run: go install github.com/yaakapp/yaakcli@latest
run: go install github.com/yaakapp/cli/cmd/yaakcli@latest
- name: Run lint
run: npm run lint

173
package-lock.json generated
View File

@@ -20,20 +20,21 @@
"@react-hook/resize-observer": "^1.2.6",
"@tailwindcss/container-queries": "^0.1.0",
"@tanstack/react-query": "^5.45.1",
"@tauri-apps/api": "^2.0.0-rc.0",
"@tauri-apps/plugin-clipboard-manager": "^2.0.0-rc.0",
"@tauri-apps/plugin-dialog": "^2.0.0-rc.0",
"@tauri-apps/plugin-fs": "^2.0.0-rc.0",
"@tauri-apps/plugin-log": "^2.0.0-rc.0",
"@tauri-apps/plugin-os": "^2.0.0-rc.0",
"@tauri-apps/plugin-shell": "^2.0.0-rc.0",
"@yaakapp/api": "^0.1.13",
"@tauri-apps/api": "^2.0.0-rc.4",
"@tauri-apps/plugin-clipboard-manager": "^2.0.0-rc.1",
"@tauri-apps/plugin-dialog": "^2.0.0-rc.1",
"@tauri-apps/plugin-fs": "^2.0.0-rc.2",
"@tauri-apps/plugin-log": "^2.0.0-rc.1",
"@tauri-apps/plugin-os": "^2.0.0-rc.1",
"@tauri-apps/plugin-shell": "^2.0.0-rc.1",
"@yaakapp/api": "^0.1.17",
"buffer": "^6.0.3",
"classnames": "^2.3.2",
"cm6-graphql": "^0.0.9",
"codemirror": "^6.0.1",
"codemirror-json-schema": "^0.6.1",
"date-fns": "^3.3.1",
"eventemitter3": "^5.0.1",
"fast-fuzzy": "^1.12.0",
"focus-trap-react": "^10.1.1",
"format-graphql": "^1.4.0",
@@ -58,7 +59,7 @@
"devDependencies": {
"@tailwindcss/nesting": "^0.0.0-insiders.565cd3e",
"@tanstack/react-query-devtools": "^5.45.1",
"@tauri-apps/cli": "^2.0.0-rc.2",
"@tauri-apps/cli": "^2.0.0-rc.12",
"@types/node": "^18.7.10",
"@types/papaparse": "^5.3.7",
"@types/parse-color": "^1.0.1",
@@ -2344,23 +2345,18 @@
}
},
"node_modules/@tauri-apps/api": {
"version": "2.0.0-rc.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.0.0-rc.0.tgz",
"integrity": "sha512-v454Qs3REHc3Za59U+/eSmBsdmF+3NE5+76+lFDaitVqN4ZglDHENDaMARYKGJVZuxiSkzyqG0SeG7lLQjVkPA==",
"engines": {
"node": ">= 18.18",
"npm": ">= 6.6.0",
"yarn": ">= 1.19.1"
},
"version": "2.0.0-rc.4",
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.0.0-rc.4.tgz",
"integrity": "sha512-UNiIhhKG08j4ooss2oEEVexffmWkgkYlC2M3GcX3VPtNsqFgVNL8Mcw/4Y7rO9M9S+ffAMnLOF5ypzyuyb8tyg==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/tauri"
}
},
"node_modules/@tauri-apps/cli": {
"version": "2.0.0-rc.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.0.0-rc.3.tgz",
"integrity": "sha512-iNF95pieBmverl1EmQyqh+fhcIClS544fN5Ex5lAbYLTiHZ/gm3lOfVBhF6NPaKd/sfLuy7K1tfDXlHztBfANw==",
"version": "2.0.0-rc.12",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.0.0-rc.12.tgz",
"integrity": "sha512-rNcVSyGHGz8vNk542isYKPk5fEMAsgmzER+1s9YYbGZCH7m4e0rH89p/P9W40I/Z4AZk4ZqjpEeajeS5izDI4g==",
"dev": true,
"bin": {
"tauri": "tauri.js"
@@ -2373,22 +2369,22 @@
"url": "https://opencollective.com/tauri"
},
"optionalDependencies": {
"@tauri-apps/cli-darwin-arm64": "2.0.0-rc.3",
"@tauri-apps/cli-darwin-x64": "2.0.0-rc.3",
"@tauri-apps/cli-linux-arm-gnueabihf": "2.0.0-rc.3",
"@tauri-apps/cli-linux-arm64-gnu": "2.0.0-rc.3",
"@tauri-apps/cli-linux-arm64-musl": "2.0.0-rc.3",
"@tauri-apps/cli-linux-x64-gnu": "2.0.0-rc.3",
"@tauri-apps/cli-linux-x64-musl": "2.0.0-rc.3",
"@tauri-apps/cli-win32-arm64-msvc": "2.0.0-rc.3",
"@tauri-apps/cli-win32-ia32-msvc": "2.0.0-rc.3",
"@tauri-apps/cli-win32-x64-msvc": "2.0.0-rc.3"
"@tauri-apps/cli-darwin-arm64": "2.0.0-rc.12",
"@tauri-apps/cli-darwin-x64": "2.0.0-rc.12",
"@tauri-apps/cli-linux-arm-gnueabihf": "2.0.0-rc.12",
"@tauri-apps/cli-linux-arm64-gnu": "2.0.0-rc.12",
"@tauri-apps/cli-linux-arm64-musl": "2.0.0-rc.12",
"@tauri-apps/cli-linux-x64-gnu": "2.0.0-rc.12",
"@tauri-apps/cli-linux-x64-musl": "2.0.0-rc.12",
"@tauri-apps/cli-win32-arm64-msvc": "2.0.0-rc.12",
"@tauri-apps/cli-win32-ia32-msvc": "2.0.0-rc.12",
"@tauri-apps/cli-win32-x64-msvc": "2.0.0-rc.12"
}
},
"node_modules/@tauri-apps/cli-darwin-arm64": {
"version": "2.0.0-rc.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.0.0-rc.3.tgz",
"integrity": "sha512-szYCSr/ChbCF+S6Wnr15TYpI2cZR07d+AQOiFGuScP0preM8Pbsk/sb0hfLwqzepjVFFNVWQba9sG7FEW2Y2XA==",
"version": "2.0.0-rc.12",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.0.0-rc.12.tgz",
"integrity": "sha512-zYxcAH4reyqKkqCAybggszFWkBvC+ZyZPTWFKXXVQ20MZc1q+e/0UJYC8UKsaumrbi1uptgamvnM8yql56x5QQ==",
"cpu": [
"arm64"
],
@@ -2402,9 +2398,9 @@
}
},
"node_modules/@tauri-apps/cli-darwin-x64": {
"version": "2.0.0-rc.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.0.0-rc.3.tgz",
"integrity": "sha512-BJv6EJOY1DJbRzVtfg8CcBAlnS5OjhBAc5YKjh4BT7EyOcop8HStBSxhL6yjWrUP7eLR1iIsW/uSehVJwzYIdQ==",
"version": "2.0.0-rc.12",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.0.0-rc.12.tgz",
"integrity": "sha512-eme7pQzEzeGCk13V3uxUNRnkVZJukqwHotqEb2RdovXqJWSyESrighBy4PBG5Xn6wNYTOyoquY9+In4TOfJAzw==",
"cpu": [
"x64"
],
@@ -2418,9 +2414,9 @@
}
},
"node_modules/@tauri-apps/cli-linux-arm-gnueabihf": {
"version": "2.0.0-rc.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.0.0-rc.3.tgz",
"integrity": "sha512-fwx805/xL4sF/EdMYqcUHQHzMYwo+OVTBTz5x/JWK8D57rnmLHAP+ZhnfFsZQLRo2QRT2l1Ye3bDyU+QRA1JFA==",
"version": "2.0.0-rc.12",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.0.0-rc.12.tgz",
"integrity": "sha512-113T2NsLeoy6GXsqc0yjMoozt+KXzkAtUB7DL9Kcvx9IMfA87cUVaTNjnb2GFsoQqpCWGfHei3nr9n1PGEbwMg==",
"cpu": [
"arm"
],
@@ -2434,9 +2430,9 @@
}
},
"node_modules/@tauri-apps/cli-linux-arm64-gnu": {
"version": "2.0.0-rc.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.0.0-rc.3.tgz",
"integrity": "sha512-3KauzO1Ls4kuY0nr82S4X8XFxlQAMN+Mqp8LLqvQ+PPMp92XQAkPH7osQdoHIEoW5gsE69U2JaiQ5tHSqNM9og==",
"version": "2.0.0-rc.12",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.0.0-rc.12.tgz",
"integrity": "sha512-9TrUyNg0vmsYF7IbG+/sybEeiz6ikA1Kjd6JjC4iwfXjRff8fuTR7CIOb06imabxbLzGP79qSAnGAeTXz8E7qA==",
"cpu": [
"arm64"
],
@@ -2450,9 +2446,9 @@
}
},
"node_modules/@tauri-apps/cli-linux-arm64-musl": {
"version": "2.0.0-rc.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.0.0-rc.3.tgz",
"integrity": "sha512-ngHS0foffm1xO5gqnDKGeYMKj8ceGmrFP5dDldoaaMQubw1SyFa0pRUjb7fZSYiO7F4SOSa8NYeMqlF9peZmnQ==",
"version": "2.0.0-rc.12",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.0.0-rc.12.tgz",
"integrity": "sha512-YvE40+wdkNcXhwUAJNPyhNzJ8YS3saJoxGj7mtNQeNeNrKhxyj6hA5T6gw9KtMkwBOp+HGtqn+eDXiu0X7BBHQ==",
"cpu": [
"arm64"
],
@@ -2466,9 +2462,9 @@
}
},
"node_modules/@tauri-apps/cli-linux-x64-gnu": {
"version": "2.0.0-rc.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.0.0-rc.3.tgz",
"integrity": "sha512-0/am9pVvuUHGmz32M8ffz1fpLnc08j3nzcRe5wUdL2AxfT+wKMII+Dn99GtCVgcdDW4jSXDMRUwrBkGocGC2OA==",
"version": "2.0.0-rc.12",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.0.0-rc.12.tgz",
"integrity": "sha512-q+MJp/lSA5WINs78dCFMlU0/jQeUkGr9GHbKeppcVcpkcY/1vog70b4KhneyvbuklKBn/V8kd0FtIKCn8VP+KQ==",
"cpu": [
"x64"
],
@@ -2482,9 +2478,9 @@
}
},
"node_modules/@tauri-apps/cli-linux-x64-musl": {
"version": "2.0.0-rc.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.0.0-rc.3.tgz",
"integrity": "sha512-r7mRi8q8TqTFVjb9kAsU7IgwUgno2s8Ip4xwq9psQhlRE3JGEZQmSEcy1jqTjfl6KFh6lJcDR7l+9/EMhL/D3Q==",
"version": "2.0.0-rc.12",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.0.0-rc.12.tgz",
"integrity": "sha512-5zodtleH2GFsB9lszDYrzPTLcr+MMqtpQpJWHATC1K03bLEA8ia8zSdBqRwm7u8NraMLl8TE7hc7hwq0uxGEcg==",
"cpu": [
"x64"
],
@@ -2498,9 +2494,9 @@
}
},
"node_modules/@tauri-apps/cli-win32-arm64-msvc": {
"version": "2.0.0-rc.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.0.0-rc.3.tgz",
"integrity": "sha512-2J6KjmDIQCw6HF1X6/yPcd+JLl7pxrH2zVMGmNllaoWhHeByvRobqFWnT7gcdHaA3dGTo432CwWvOgTgrINQpQ==",
"version": "2.0.0-rc.12",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.0.0-rc.12.tgz",
"integrity": "sha512-nSu6VHpuq61DYM2YowLDLDwkK8im7dzYxIHXs+h8/rhkmadTujGhbyUhHPI1STA6hNyITUtSFpo6P2mEbfpAIg==",
"cpu": [
"arm64"
],
@@ -2514,9 +2510,9 @@
}
},
"node_modules/@tauri-apps/cli-win32-ia32-msvc": {
"version": "2.0.0-rc.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.0.0-rc.3.tgz",
"integrity": "sha512-8q75CsHDSEDdgi6xPwim+BaQZFCswK2Dn/qL38V3Mh9kmVvC8oGJMPC66bC20dF+v3KWeFm2FNNGQqOSXCveHg==",
"version": "2.0.0-rc.12",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.0.0-rc.12.tgz",
"integrity": "sha512-d/4y57OisMuB+MUkTpZsryQRi9ZQXQ8SsMhrvEgu8sbX8/WRm0iZyGuIZ01RlZZHLIasXbKTkPX+hPQC5Juk8Q==",
"cpu": [
"ia32"
],
@@ -2530,9 +2526,9 @@
}
},
"node_modules/@tauri-apps/cli-win32-x64-msvc": {
"version": "2.0.0-rc.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.0.0-rc.3.tgz",
"integrity": "sha512-qeBRJYalahxEXolekcpZJ/HBrIJacG2NWJBGhhi797mIwnbmlpbHMc8blIJtNNNwVUb2BjXuxKQVfojQ5YYrcg==",
"version": "2.0.0-rc.12",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.0.0-rc.12.tgz",
"integrity": "sha512-RsPUvsbFza03ysh0nU2nM3P2CVWz9cu7CRHwOEdtXjWWNREHUYCaVpqQKz0tn2sG19yXiNIB40iqrIBUmb/IoA==",
"cpu": [
"x64"
],
@@ -2546,51 +2542,51 @@
}
},
"node_modules/@tauri-apps/plugin-clipboard-manager": {
"version": "2.0.0-rc.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-clipboard-manager/-/plugin-clipboard-manager-2.0.0-rc.0.tgz",
"integrity": "sha512-2fS3wbRQEtorkk3Np2msJUeKCXRqLQ9sSo2FzlFdUPYNzThsu43uWCF55McGLAfltNOvXQIcQLUBf05jbBL/5w==",
"version": "2.0.0-rc.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-clipboard-manager/-/plugin-clipboard-manager-2.0.0-rc.1.tgz",
"integrity": "sha512-hFgUABMmQuVGKwHb8PR9fuqfk0WRkedbWUt/ZV5sL4Q6kLrsp3JYJvtzVPeMYdeBvMqHl8WXNxAc/zwSld2h9w==",
"dependencies": {
"@tauri-apps/api": "^2.0.0-rc.0"
"@tauri-apps/api": "^2.0.0-rc.4"
}
},
"node_modules/@tauri-apps/plugin-dialog": {
"version": "2.0.0-rc.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.0.0-rc.0.tgz",
"integrity": "sha512-DPOXYe8SQ6Radk/67EOdaomlxL7oF99JO/ZUaPp1IBEs3Wro7lhlz63CfdKIBfKIZTLJLzP1R7/EiPL/GTA3Bg==",
"version": "2.0.0-rc.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.0.0-rc.1.tgz",
"integrity": "sha512-H28gh6BfZtjflHQ+HrmWwunDriBI3AQLAKnMs50GA6zeNUULqbQr7VXbAAKeJL/0CmWcecID4PKXVoSlaWRhEg==",
"dependencies": {
"@tauri-apps/api": "^2.0.0-rc.0"
"@tauri-apps/api": "^2.0.0-rc.4"
}
},
"node_modules/@tauri-apps/plugin-fs": {
"version": "2.0.0-rc.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-fs/-/plugin-fs-2.0.0-rc.0.tgz",
"integrity": "sha512-74VCXEZlzTJ+Jv1V3KrV0qIHhSePpE/ljsF78rcEuvSfyTxLtt/Sb5CIUmVhFlKTRFOH9dX50T4dTZ3qFLyRnA==",
"version": "2.0.0-rc.2",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-fs/-/plugin-fs-2.0.0-rc.2.tgz",
"integrity": "sha512-TFjCfso3tN4b5s2EBjqP8N2gYrPh93Ds3VNKj8pCXv4wbvnItyfG0aHO0haUsedBOHQryDwv9vDAdPX6/T0a+g==",
"dependencies": {
"@tauri-apps/api": "^2.0.0-rc.0"
"@tauri-apps/api": "^2.0.0-rc.4"
}
},
"node_modules/@tauri-apps/plugin-log": {
"version": "2.0.0-rc.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-log/-/plugin-log-2.0.0-rc.0.tgz",
"integrity": "sha512-ztKfzUcq03dtr08vBu+xcwIEPusP6mCpuLAt6kpXEwG+HvYsC8e1/KFFokn3xvfwD+oBJ3UTL1h4kdM30GAqGw==",
"version": "2.0.0-rc.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-log/-/plugin-log-2.0.0-rc.1.tgz",
"integrity": "sha512-+Tz0zo4FDtC/5j7neeIq5ievgKbUXBV2+X5HtbaR8ZZ2bcksCp8UqeHd6cyyN+FSk4qaU01LIGkuExtxk1h/FA==",
"dependencies": {
"@tauri-apps/api": "^2.0.0-rc.0"
"@tauri-apps/api": "^2.0.0-rc.4"
}
},
"node_modules/@tauri-apps/plugin-os": {
"version": "2.0.0-rc.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-os/-/plugin-os-2.0.0-rc.0.tgz",
"integrity": "sha512-OWAl8mooKnGykSD4iog8WRqcnOSx0gGmTJBlEExHdFeIuOHg0Ezvd+WiVLhT9LBg7go3ibNWRWpe/ZG7YEp4Vw==",
"version": "2.0.0-rc.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-os/-/plugin-os-2.0.0-rc.1.tgz",
"integrity": "sha512-PV8zlSTmYfiN2xzILUmlDSEycS7UYbH2yXk/ZqF+qQU6/s+OVQvmSth4EhllFjcpvPbtqELvpzfjw+2qEouchA==",
"dependencies": {
"@tauri-apps/api": "^2.0.0-rc.0"
"@tauri-apps/api": "^2.0.0-rc.4"
}
},
"node_modules/@tauri-apps/plugin-shell": {
"version": "2.0.0-rc.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-shell/-/plugin-shell-2.0.0-rc.0.tgz",
"integrity": "sha512-bhUcQcrqZoK8H1DFXapr5r1Z75oh6Kd5Tltz97XpZFLREEqp+KhN2Fvyh8r/fKAyenYsTYUIsDsyGdjdueuF9g==",
"version": "2.0.0-rc.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-shell/-/plugin-shell-2.0.0-rc.1.tgz",
"integrity": "sha512-JtNROc0rqEwN/g93ig5pK4cl1vUo2yn+osCpY9de64cy/d9hRzof7AuYOgvt/Xcd5VPQmlgo2AGvUh5sQRSR1A==",
"dependencies": {
"@tauri-apps/api": "^2.0.0-rc.0"
"@tauri-apps/api": "^2.0.0-rc.4"
}
},
"node_modules/@types/babel__core": {
@@ -2990,9 +2986,9 @@
"integrity": "sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ=="
},
"node_modules/@yaakapp/api": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/@yaakapp/api/-/api-0.1.13.tgz",
"integrity": "sha512-FSYPHZV0mP967w63VXi9zYP81hPo3vjSW3/UElJLuF/8ig6WmG4p1q2oYos4Ik267Z3qSQAGN5dPMfuk3DAnBA==",
"version": "0.1.17",
"resolved": "https://registry.npmjs.org/@yaakapp/api/-/api-0.1.17.tgz",
"integrity": "sha512-VE9A0FDZwczZkTAbMOYjQOKzbW1KmaItq/mPSuTgU87Lf570JUepcHVtL7QFLV1U/R5q+n7I6xQg9Q2mDj/OWQ==",
"dependencies": {
"@types/node": "^22.0.0"
}
@@ -5750,8 +5746,7 @@
"node_modules/eventemitter3": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
"dev": true
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="
},
"node_modules/execa": {
"version": "7.2.0",

View File

@@ -35,20 +35,21 @@
"@react-hook/resize-observer": "^1.2.6",
"@tailwindcss/container-queries": "^0.1.0",
"@tanstack/react-query": "^5.45.1",
"@tauri-apps/api": "^2.0.0-rc.0",
"@tauri-apps/plugin-clipboard-manager": "^2.0.0-rc.0",
"@tauri-apps/plugin-dialog": "^2.0.0-rc.0",
"@tauri-apps/plugin-fs": "^2.0.0-rc.0",
"@tauri-apps/plugin-log": "^2.0.0-rc.0",
"@tauri-apps/plugin-os": "^2.0.0-rc.0",
"@tauri-apps/plugin-shell": "^2.0.0-rc.0",
"@yaakapp/api": "^0.1.13",
"@tauri-apps/api": "^2.0.0-rc.4",
"@tauri-apps/plugin-clipboard-manager": "^2.0.0-rc.1",
"@tauri-apps/plugin-dialog": "^2.0.0-rc.1",
"@tauri-apps/plugin-fs": "^2.0.0-rc.2",
"@tauri-apps/plugin-log": "^2.0.0-rc.1",
"@tauri-apps/plugin-os": "^2.0.0-rc.1",
"@tauri-apps/plugin-shell": "^2.0.0-rc.1",
"@yaakapp/api": "^0.1.17",
"buffer": "^6.0.3",
"classnames": "^2.3.2",
"cm6-graphql": "^0.0.9",
"codemirror": "^6.0.1",
"codemirror-json-schema": "^0.6.1",
"date-fns": "^3.3.1",
"eventemitter3": "^5.0.1",
"fast-fuzzy": "^1.12.0",
"focus-trap-react": "^10.1.1",
"format-graphql": "^1.4.0",
@@ -73,7 +74,7 @@
"devDependencies": {
"@tailwindcss/nesting": "^0.0.0-insiders.565cd3e",
"@tanstack/react-query-devtools": "^5.45.1",
"@tauri-apps/cli": "^2.0.0-rc.2",
"@tauri-apps/cli": "^2.0.0-rc.12",
"@types/node": "^18.7.10",
"@types/papaparse": "^5.3.7",
"@types/parse-color": "^1.0.1",

View File

@@ -1,6 +1,6 @@
{
"name": "@yaakapp/api",
"version": "0.1.13",
"version": "0.1.17",
"main": "lib/index.js",
"typings": "./lib/index.d.ts",
"files": [

View File

@@ -1,4 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { GrpcEventType } from "./GrpcEventType";
export type GrpcEvent = { id: string, model: "grpc_event", workspaceId: string, requestId: string, connectionId: string, createdAt: string, content: string, eventType: GrpcEventType, metadata: { [key: string]: string }, status: number | null, error: string | null, };
export type GrpcEvent = { id: string, model: "grpc_event", workspaceId: string, requestId: string, connectionId: string, createdAt: string, updatedAt: string, content: string, eventType: GrpcEventType, metadata: { [key: string]: string }, status: number | null, error: string | null, };

View File

@@ -1,6 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { BootRequest } from "./BootRequest";
import type { BootResponse } from "./BootResponse";
import type { CallHttpRequestActionRequest } from "./CallHttpRequestActionRequest";
import type { CallTemplateFunctionRequest } from "./CallTemplateFunctionRequest";
import type { CallTemplateFunctionResponse } from "./CallTemplateFunctionResponse";
@@ -19,10 +17,12 @@ import type { GetHttpRequestByIdResponse } from "./GetHttpRequestByIdResponse";
import type { GetTemplateFunctionsResponse } from "./GetTemplateFunctionsResponse";
import type { ImportRequest } from "./ImportRequest";
import type { ImportResponse } from "./ImportResponse";
import type { PluginBootRequest } from "./PluginBootRequest";
import type { PluginBootResponse } from "./PluginBootResponse";
import type { RenderHttpRequestRequest } from "./RenderHttpRequestRequest";
import type { RenderHttpRequestResponse } from "./RenderHttpRequestResponse";
import type { SendHttpRequestRequest } from "./SendHttpRequestRequest";
import type { SendHttpRequestResponse } from "./SendHttpRequestResponse";
import type { ShowToastRequest } from "./ShowToastRequest";
export type InternalEventPayload = { "type": "boot_request" } & BootRequest | { "type": "boot_response" } & BootResponse | { "type": "import_request" } & ImportRequest | { "type": "import_response" } & ImportResponse | { "type": "filter_request" } & FilterRequest | { "type": "filter_response" } & FilterResponse | { "type": "export_http_request_request" } & ExportHttpRequestRequest | { "type": "export_http_request_response" } & ExportHttpRequestResponse | { "type": "send_http_request_request" } & SendHttpRequestRequest | { "type": "send_http_request_response" } & SendHttpRequestResponse | { "type": "get_http_request_actions_request" } & GetHttpRequestActionsRequest | { "type": "get_http_request_actions_response" } & GetHttpRequestActionsResponse | { "type": "call_http_request_action_request" } & CallHttpRequestActionRequest | { "type": "get_template_functions_request" } | { "type": "get_template_functions_response" } & GetTemplateFunctionsResponse | { "type": "call_template_function_request" } & CallTemplateFunctionRequest | { "type": "call_template_function_response" } & CallTemplateFunctionResponse | { "type": "copy_text_request" } & CopyTextRequest | { "type": "render_http_request_request" } & RenderHttpRequestRequest | { "type": "render_http_request_response" } & RenderHttpRequestResponse | { "type": "show_toast_request" } & ShowToastRequest | { "type": "get_http_request_by_id_request" } & GetHttpRequestByIdRequest | { "type": "get_http_request_by_id_response" } & GetHttpRequestByIdResponse | { "type": "find_http_responses_request" } & FindHttpResponsesRequest | { "type": "find_http_responses_response" } & FindHttpResponsesResponse | { "type": "empty_response" } & EmptyResponse;
export type InternalEventPayload = { "type": "boot_request" } & PluginBootRequest | { "type": "boot_response" } & PluginBootResponse | { "type": "import_request" } & ImportRequest | { "type": "import_response" } & ImportResponse | { "type": "filter_request" } & FilterRequest | { "type": "filter_response" } & FilterResponse | { "type": "export_http_request_request" } & ExportHttpRequestRequest | { "type": "export_http_request_response" } & ExportHttpRequestResponse | { "type": "send_http_request_request" } & SendHttpRequestRequest | { "type": "send_http_request_response" } & SendHttpRequestResponse | { "type": "get_http_request_actions_request" } & GetHttpRequestActionsRequest | { "type": "get_http_request_actions_response" } & GetHttpRequestActionsResponse | { "type": "call_http_request_action_request" } & CallHttpRequestActionRequest | { "type": "get_template_functions_request" } | { "type": "get_template_functions_response" } & GetTemplateFunctionsResponse | { "type": "call_template_function_request" } & CallTemplateFunctionRequest | { "type": "call_template_function_response" } & CallTemplateFunctionResponse | { "type": "copy_text_request" } & CopyTextRequest | { "type": "render_http_request_request" } & RenderHttpRequestRequest | { "type": "render_http_request_response" } & RenderHttpRequestResponse | { "type": "show_toast_request" } & ShowToastRequest | { "type": "get_http_request_by_id_request" } & GetHttpRequestByIdRequest | { "type": "get_http_request_by_id_response" } & GetHttpRequestByIdResponse | { "type": "find_http_responses_request" } & FindHttpResponsesRequest | { "type": "find_http_responses_response" } & FindHttpResponsesResponse | { "type": "empty_response" } & EmptyResponse;

View File

@@ -8,7 +8,8 @@ import type { GrpcRequest } from "./GrpcRequest";
import type { HttpRequest } from "./HttpRequest";
import type { HttpResponse } from "./HttpResponse";
import type { KeyValue } from "./KeyValue";
import type { Plugin } from "./Plugin";
import type { Settings } from "./Settings";
import type { Workspace } from "./Workspace";
export type Model = Environment | Folder | GrpcConnection | GrpcEvent | GrpcRequest | HttpRequest | HttpResponse | KeyValue | Workspace | CookieJar | Settings;
export type Model = Environment | Folder | GrpcConnection | GrpcEvent | GrpcRequest | HttpRequest | HttpResponse | KeyValue | Workspace | CookieJar | Settings | Plugin;

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type Plugin = { id: string, model: "plugin", createdAt: string, updatedAt: string, checkedAt: string | null, name: string, version: string, uri: string, enabled: boolean, };

View File

@@ -1,3 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type BootRequest = { dir: string, };
export type PluginBootRequest = { dir: string, };

View File

@@ -1,3 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type BootResponse = { name: string, version: string, capabilities: Array<string>, };
export type PluginBootResponse = { name: string, version: string, capabilities: Array<string>, };

View File

@@ -1,3 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type RenderRequest = { template: string, };

View File

@@ -1,3 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type RenderResponse = { rendered: string, };

View File

@@ -2,8 +2,6 @@ export type * from './plugins';
export type * from './themes';
// TODO: The next ts-rs release includes the ability to put everything in 1 file!
export * from './gen/BootRequest';
export * from './gen/BootResponse';
export * from './gen/CallHttpRequestActionArgs';
export * from './gen/CallHttpRequestActionRequest';
export * from './gen/CallTemplateFunctionRequest';
@@ -45,6 +43,8 @@ export * from './gen/InternalEvent';
export * from './gen/InternalEventPayload';
export * from './gen/KeyValue';
export * from './gen/Model';
export * from './gen/PluginBootRequest';
export * from './gen/PluginBootResponse';
export * from './gen/RenderHttpRequestRequest';
export * from './gen/RenderHttpRequestResponse';
export * from './gen/RenderPurpose';
@@ -63,3 +63,4 @@ export * from './gen/TemplateFunctionSelectOption';
export * from './gen/TemplateFunctionTextArg';
export * from './gen/ToastVariant';
export * from './gen/Workspace';
export * from './gen/Plugin';

View File

@@ -9,7 +9,7 @@ export type { Context } from './Context';
/**
* The global structure of a Yaak plugin
*/
export type Plugin = {
export type PluginDefinition = {
importer?: ImporterPlugin;
theme?: ThemePlugin;
filter?: FilterPlugin;

402
src-tauri/Cargo.lock generated
View File

@@ -124,7 +124,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fb4009533e8ff8f1450a5bcbc30f4242a1d34442221f72314bea1f5dc9c7f89"
dependencies = [
"clipboard-win",
"core-graphics",
"core-graphics 0.23.2",
"image 0.25.2",
"log",
"objc2",
@@ -188,7 +188,7 @@ version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fec134f64e2bc57411226dfc4e52dec859ddfc7e711fc5e07b612584f000e4aa"
dependencies = [
"brotli 6.0.0",
"brotli",
"flate2",
"futures-core",
"memchr",
@@ -577,17 +577,6 @@ dependencies = [
"syn_derive",
]
[[package]]
name = "brotli"
version = "3.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d640d25bc63c50fb1f0b545ffd80207d2e10a4c965530809b40ba3386825c391"
dependencies = [
"alloc-no-stdlib",
"alloc-stdlib",
"brotli-decompressor 2.5.1",
]
[[package]]
name = "brotli"
version = "6.0.0"
@@ -596,17 +585,7 @@ checksum = "74f7971dbd9326d58187408ab83117d8ac1bb9c17b085fdacd1cf2f598719b6b"
dependencies = [
"alloc-no-stdlib",
"alloc-stdlib",
"brotli-decompressor 4.0.1",
]
[[package]]
name = "brotli-decompressor"
version = "2.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e2e4afe60d7dd600fdd3de8d0f08c2b7ec039712e3b6137ff98b7004e82de4f"
dependencies = [
"alloc-no-stdlib",
"alloc-stdlib",
"brotli-decompressor",
]
[[package]]
@@ -835,9 +814,25 @@ checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c"
dependencies = [
"bitflags 1.3.2",
"block",
"cocoa-foundation",
"core-foundation",
"core-graphics",
"cocoa-foundation 0.1.2",
"core-foundation 0.9.4",
"core-graphics 0.23.2",
"foreign-types 0.5.0",
"libc",
"objc",
]
[[package]]
name = "cocoa"
version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f79398230a6e2c08f5c9760610eb6924b52aa9e7950a619602baba59dcbbdbb2"
dependencies = [
"bitflags 2.6.0",
"block",
"cocoa-foundation 0.2.0",
"core-foundation 0.10.0",
"core-graphics 0.24.0",
"foreign-types 0.5.0",
"libc",
"objc",
@@ -851,8 +846,22 @@ checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7"
dependencies = [
"bitflags 1.3.2",
"block",
"core-foundation",
"core-graphics-types",
"core-foundation 0.9.4",
"core-graphics-types 0.1.3",
"libc",
"objc",
]
[[package]]
name = "cocoa-foundation"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e14045fb83be07b5acf1c0884b2180461635b433455fa35d1cd6f17f1450679d"
dependencies = [
"bitflags 2.6.0",
"block",
"core-foundation 0.10.0",
"core-graphics-types 0.2.0",
"libc",
"objc",
]
@@ -954,10 +963,20 @@ dependencies = [
]
[[package]]
name = "core-foundation-sys"
version = "0.8.6"
name = "core-foundation"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "core-graphics"
@@ -966,8 +985,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081"
dependencies = [
"bitflags 1.3.2",
"core-foundation",
"core-graphics-types",
"core-foundation 0.9.4",
"core-graphics-types 0.1.3",
"foreign-types 0.5.0",
"libc",
]
[[package]]
name = "core-graphics"
version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1"
dependencies = [
"bitflags 2.6.0",
"core-foundation 0.10.0",
"core-graphics-types 0.2.0",
"foreign-types 0.5.0",
"libc",
]
@@ -979,7 +1011,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf"
dependencies = [
"bitflags 1.3.2",
"core-foundation",
"core-foundation 0.9.4",
"libc",
]
[[package]]
name = "core-graphics-types"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb"
dependencies = [
"bitflags 2.6.0",
"core-foundation 0.10.0",
"libc",
]
@@ -1596,6 +1639,15 @@ dependencies = [
"miniz_oxide",
]
[[package]]
name = "fluent-uri"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17c704e9dbe1ddd863da1e6ff3567795087b1eb201ce80d8fa81162e1516500d"
dependencies = [
"bitflags 1.3.2",
]
[[package]]
name = "flume"
version = "0.11.0"
@@ -2588,9 +2640,9 @@ dependencies = [
[[package]]
name = "infer"
version = "0.15.0"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb33622da908807a06f9513c19b3c1ad50fab3e4137d82a78107d502075aa199"
checksum = "bc150e5ce2330295b8616ce0e3f53250e53af31759a9dbedad1621ba29151847"
dependencies = [
"cfb",
]
@@ -2755,15 +2807,27 @@ dependencies = [
[[package]]
name = "json-patch"
version = "1.4.0"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec9ad60d674508f3ca8f380a928cfe7b096bc729c4e2dbfe3852bc45da3ab30b"
checksum = "5b1fb8864823fad91877e6caea0baca82e49e8db50f8e5c9f9a453e27d3330fc"
dependencies = [
"jsonptr",
"serde",
"serde_json",
"thiserror",
]
[[package]]
name = "jsonptr"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c6e529149475ca0b2820835d3dce8fcc41c6b943ca608d32f35b449255e4627"
dependencies = [
"fluent-uri",
"serde",
"serde_json",
]
[[package]]
name = "keyboard-types"
version = "0.7.0"
@@ -3053,11 +3117,11 @@ dependencies = [
[[package]]
name = "muda"
version = "0.13.5"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86b959f97c97044e4c96e32e1db292a7d594449546a3c6b77ae613dc3a5b5145"
checksum = "ba8ac4080fb1e097c2c22acae467e46e4da72d941f02e82b67a87a2a89fa38b1"
dependencies = [
"cocoa",
"cocoa 0.26.0",
"crossbeam-channel",
"dpi",
"gtk",
@@ -3067,7 +3131,7 @@ dependencies = [
"png",
"serde",
"thiserror",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -3095,15 +3159,16 @@ dependencies = [
[[package]]
name = "ndk"
version = "0.7.0"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "451422b7e4718271c8b5b3aadf5adedba43dc76312454b387e98fae0fc951aa0"
checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4"
dependencies = [
"bitflags 1.3.2",
"bitflags 2.6.0",
"jni-sys",
"log",
"ndk-sys",
"num_enum",
"raw-window-handle 0.5.2",
"raw-window-handle",
"thiserror",
]
@@ -3115,9 +3180,9 @@ checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b"
[[package]]
name = "ndk-sys"
version = "0.4.1+23.1.7779620"
version = "0.6.0+11769913"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3cf2aae958bd232cac5069850591667ad422d263686d75b52a065f9badeee5a3"
checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873"
dependencies = [
"jni-sys",
]
@@ -3231,23 +3296,23 @@ dependencies = [
[[package]]
name = "num_enum"
version = "0.5.11"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9"
checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179"
dependencies = [
"num_enum_derive",
]
[[package]]
name = "num_enum_derive"
version = "0.5.11"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799"
checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56"
dependencies = [
"proc-macro-crate 1.3.1",
"proc-macro-crate 2.0.2",
"proc-macro2",
"quote",
"syn 1.0.109",
"syn 2.0.72",
]
[[package]]
@@ -4302,12 +4367,6 @@ dependencies = [
"rand_core 0.5.1",
]
[[package]]
name = "raw-window-handle"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9"
[[package]]
name = "raw-window-handle"
version = "0.6.2"
@@ -4505,7 +4564,7 @@ dependencies = [
"objc",
"objc-foundation",
"objc_id",
"raw-window-handle 0.6.2",
"raw-window-handle",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
@@ -4872,7 +4931,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
dependencies = [
"bitflags 2.6.0",
"core-foundation",
"core-foundation 0.9.4",
"core-foundation-sys",
"libc",
"security-framework-sys",
@@ -5202,7 +5261,7 @@ checksum = "d623bff5d06f60d738990980d782c8c866997d9194cfe79ecad00aa2f76826dd"
dependencies = [
"bytemuck",
"cfg_aliases 0.2.1",
"core-graphics",
"core-graphics 0.23.2",
"foreign-types 0.5.0",
"js-sys",
"log",
@@ -5210,7 +5269,7 @@ dependencies = [
"objc2-app-kit",
"objc2-foundation",
"objc2-quartz-core",
"raw-window-handle 0.6.2",
"raw-window-handle",
"redox_syscall 0.5.3",
"wasm-bindgen",
"web-sys",
@@ -5541,9 +5600,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "swift-rs"
version = "1.0.6"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bbdb58577b6301f8d17ae2561f32002a5bae056d444e0f69e611e504a276204"
checksum = "4057c98e2e852d51fdcfca832aac7b571f6b351ad159f9eda5db1655f8d0c4d7"
dependencies = [
"base64 0.21.7",
"serde",
@@ -5612,7 +5671,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
dependencies = [
"bitflags 1.3.2",
"core-foundation",
"core-foundation 0.9.4",
"system-configuration-sys",
]
@@ -5641,14 +5700,14 @@ dependencies = [
[[package]]
name = "tao"
version = "0.28.1"
version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea538df05fbc2dcbbd740ba0cfe8607688535f4798d213cbbfa13ce494f3451f"
checksum = "2a93f2c6b8fdaeb7f417bda89b5bc767999745c3052969664ae1fa65892deb7e"
dependencies = [
"bitflags 2.6.0",
"cocoa",
"core-foundation",
"core-graphics",
"cocoa 0.26.0",
"core-foundation 0.10.0",
"core-graphics 0.24.0",
"crossbeam-channel",
"dispatch",
"dlopen2",
@@ -5667,13 +5726,13 @@ dependencies = [
"objc",
"once_cell",
"parking_lot",
"raw-window-handle 0.6.2",
"raw-window-handle",
"scopeguard",
"tao-macros",
"unicode-segmentation",
"url",
"windows 0.57.0",
"windows-core 0.57.0",
"windows 0.58.0",
"windows-core 0.58.0",
"windows-version",
"x11-dl",
]
@@ -5714,13 +5773,13 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
[[package]]
name = "tauri"
version = "2.0.0-rc.2"
version = "2.0.0-rc.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19ee93e545e49458813d4ed16179c67ee6141dba140ec3d4f078dda3b8d4e0d1"
checksum = "a6327f79726c508efbbc3826b343fd7d39ebce786bdeff5881077b35d335d9e0"
dependencies = [
"anyhow",
"bytes",
"cocoa",
"cocoa 0.26.0",
"dirs",
"dunce",
"embed_plist",
@@ -5738,7 +5797,7 @@ dependencies = [
"muda",
"objc",
"percent-encoding",
"raw-window-handle 0.6.2",
"raw-window-handle",
"reqwest",
"serde",
"serde_json",
@@ -5759,14 +5818,14 @@ dependencies = [
"webkit2gtk",
"webview2-com",
"window-vibrancy",
"windows 0.57.0",
"windows 0.58.0",
]
[[package]]
name = "tauri-build"
version = "2.0.0-rc.2"
version = "2.0.0-rc.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96a58b3a716b51d7f671f729bb8c0a53cd2551eec8450c64e828ef4e6c9f948e"
checksum = "7938a610d1474435fa38dfba66c95ce9be7f17b500672b6e00072bca5e52fef3"
dependencies = [
"anyhow",
"cargo_toml",
@@ -5786,12 +5845,12 @@ dependencies = [
[[package]]
name = "tauri-codegen"
version = "2.0.0-rc.2"
version = "2.0.0-rc.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90a9e63ecd827d57228864764e0234935c9aac230099cf145197c8c08e754ced"
checksum = "467d3e95b57c860bea13b7c812820d9e7425e4b700e5e69b358d906f22022007"
dependencies = [
"base64 0.22.1",
"brotli 3.5.0",
"brotli",
"ico",
"json-patch",
"plist",
@@ -5813,9 +5872,9 @@ dependencies = [
[[package]]
name = "tauri-macros"
version = "2.0.0-rc.2"
version = "2.0.0-rc.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a54f5d5b289aa6215ffcfed7d4ff9960a04b7a854436d04519a9fcf911050cba"
checksum = "c4585a906bd96bf57d063c3d60c52577ccc5de592d08f9d112e873ada79af9b9"
dependencies = [
"heck 0.5.0",
"proc-macro2",
@@ -5827,9 +5886,9 @@ dependencies = [
[[package]]
name = "tauri-plugin"
version = "2.0.0-rc.2"
version = "2.0.0-rc.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03ce2ac5e182251ff932750d69c9b240a78e44901a7a6234814d63c595b43660"
checksum = "b905ecef194245bb35baba0447703f9fe40e4c03e946c7aba54c21c23e3452c7"
dependencies = [
"anyhow",
"glob",
@@ -5860,13 +5919,12 @@ dependencies = [
[[package]]
name = "tauri-plugin-dialog"
version = "2.0.0-rc.0"
version = "2.0.0-rc.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c538457a755a75b8bb1594ed40d1512f8f6386251d3fcde492f8f46768ec85b"
checksum = "0e0c5b6a7ea7db72623e9a6f7e5a8044dae4bb9cb3d154557cb082d10288bc9b"
dependencies = [
"dunce",
"log",
"raw-window-handle 0.6.2",
"raw-window-handle",
"rfd",
"serde",
"serde_json",
@@ -5874,15 +5932,17 @@ dependencies = [
"tauri-plugin",
"tauri-plugin-fs",
"thiserror",
"url",
]
[[package]]
name = "tauri-plugin-fs"
version = "2.0.0-rc.0"
version = "2.0.0-rc.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5df6b25b1f2b7b61565e66c4dbee9eb39e5635d2a763206e380e07cc3f601a67"
checksum = "8a4fa21faf29a0b18f1ab509c2f75f9b793ec2ebdbc9a81b75f9c8f1399f20ae"
dependencies = [
"anyhow",
"dunce",
"glob",
"schemars",
"serde",
@@ -5897,13 +5957,13 @@ dependencies = [
[[package]]
name = "tauri-plugin-log"
version = "2.0.0-rc.0"
version = "2.0.0-rc.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "380d27f23c39cde6a73024e65d8ec9b5b0af861e968dbe16b3aad86cd2c578e5"
checksum = "b57e4666c4a5d81f81b7bb8eacf51ae32c4e69c35071aabb480ad20a80836e4e"
dependencies = [
"android_logger",
"byte-unit",
"cocoa",
"cocoa 0.25.0",
"fern",
"log",
"objc",
@@ -5919,9 +5979,9 @@ dependencies = [
[[package]]
name = "tauri-plugin-os"
version = "2.0.0-rc.0"
version = "2.0.0-rc.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b54cfeb26356822d3be3db4282041b03552f573a694b6b28aded7d95c62a039"
checksum = "ebc4ee761edd532fce2232453e9c8e0f7d9c0b6fe125c4b90b3eb4362ee84224"
dependencies = [
"gethostname",
"log",
@@ -5937,9 +5997,9 @@ dependencies = [
[[package]]
name = "tauri-plugin-shell"
version = "2.0.0-rc.0"
version = "2.0.0-rc.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9209f6c32caec61e156a5616f7d80ba7683ca4a0a5641cbe5d3086ab371aaab2"
checksum = "e83800ddf78b820172efb5ed7310344e8e4f97fd30cd8237a3f20c12a79eb136"
dependencies = [
"encoding_rs",
"log",
@@ -5958,9 +6018,9 @@ dependencies = [
[[package]]
name = "tauri-plugin-updater"
version = "2.0.0-rc.0"
version = "2.0.0-rc.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b5f10ba18d2fc65e16bdf053b7beccb621dcf880c52d2ab08bdeb2d685e3e14"
checksum = "391ebb8ae8cd6aec44b5d96d3005659d88cde69c57326f639bbc660116a30d63"
dependencies = [
"base64 0.22.1",
"dirs",
@@ -5981,15 +6041,15 @@ dependencies = [
"time",
"tokio",
"url",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
"zip",
]
[[package]]
name = "tauri-plugin-window-state"
version = "2.0.0-rc.0"
version = "2.0.0-rc.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a86654a4937427217c7531594a60bede289e0e6d27baef0527d5678fb3d263a"
checksum = "303569dd7858361d4c623845448b136b4c95d53b5d2bde6630bea9d7f0022d45"
dependencies = [
"bitflags 2.6.0",
"log",
@@ -6002,36 +6062,36 @@ dependencies = [
[[package]]
name = "tauri-runtime"
version = "2.0.0-rc.2"
version = "2.0.0-rc.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f01b129b1ebdf09563c354760dbe7c0e96a166b4e33362d9c8d207f527c7ea5"
checksum = "b72cd110a6699ef44963504d4fa4f6c535677bb0177da2d178f4f822a53058ed"
dependencies = [
"dpi",
"gtk",
"http 1.1.0",
"jni",
"raw-window-handle 0.6.2",
"raw-window-handle",
"serde",
"serde_json",
"tauri-utils",
"thiserror",
"url",
"windows 0.57.0",
"windows 0.58.0",
]
[[package]]
name = "tauri-runtime-wry"
version = "2.0.0-rc.2"
version = "2.0.0-rc.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcda27639094ace2bf25f00bc10e35ea4e3af2f92753b1bdd2a174d1fa5a6292"
checksum = "1eb325cca17496ccbb469e7e2fef7f3e31a1005ab0c658dc3331c7781a573401"
dependencies = [
"cocoa",
"cocoa 0.26.0",
"gtk",
"http 1.1.0",
"jni",
"log",
"percent-encoding",
"raw-window-handle 0.6.2",
"raw-window-handle",
"softbuffer",
"tao",
"tauri-runtime",
@@ -6039,17 +6099,17 @@ dependencies = [
"url",
"webkit2gtk",
"webview2-com",
"windows 0.57.0",
"windows 0.58.0",
"wry",
]
[[package]]
name = "tauri-utils"
version = "2.0.0-rc.2"
version = "2.0.0-rc.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28bb83cffa26e9cb7a2b3d0c31ab87bf277f44aaaa90f17159aef4d37aabd051"
checksum = "6746b87c4755f493b94920622e245aef2d771f001ddeffc203e315872d323e1c"
dependencies = [
"brotli 3.5.0",
"brotli",
"cargo_metadata",
"ctor",
"dunce",
@@ -6548,22 +6608,23 @@ dependencies = [
[[package]]
name = "tray-icon"
version = "0.14.3"
version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ad8319cca93189ea9ab1b290de0595960529750b6b8b501a399ed1ec3775d60"
checksum = "044d7738b3d50f288ddef035b793228740ad4d927f5466b0af55dc15e7e03cfe"
dependencies = [
"cocoa",
"core-graphics",
"core-graphics 0.24.0",
"crossbeam-channel",
"dirs",
"libappindicator",
"muda",
"objc",
"objc2",
"objc2-app-kit",
"objc2-foundation",
"once_cell",
"png",
"serde",
"thiserror",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -6739,12 +6800,17 @@ dependencies = [
]
[[package]]
name = "urlpattern"
version = "0.2.0"
name = "urlencoding"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9bd5ff03aea02fa45b13a7980151fe45009af1980ba69f651ec367121a31609"
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
[[package]]
name = "urlpattern"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d"
dependencies = [
"derive_more",
"regex",
"serde",
"unic-ucd-ident",
@@ -7016,23 +7082,23 @@ dependencies = [
[[package]]
name = "webview2-com"
version = "0.31.0"
version = "0.33.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6516cfa64c6b3212686080eeec378e662c2af54bb2a5b2a22749673f5cb2226f"
checksum = "6f61ff3d9d0ee4efcb461b14eb3acfda2702d10dc329f339303fc3e57215ae2c"
dependencies = [
"webview2-com-macros",
"webview2-com-sys",
"windows 0.57.0",
"windows-core 0.57.0",
"windows 0.58.0",
"windows-core 0.58.0",
"windows-implement",
"windows-interface",
]
[[package]]
name = "webview2-com-macros"
version = "0.7.0"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac1345798ecd8122468840bcdf1b95e5dc6d2206c5e4b0eafa078d061f59c9bc"
checksum = "1d228f15bba3b9d56dde8bddbee66fa24545bd17b48d5128ccf4a8742b18e431"
dependencies = [
"proc-macro2",
"quote",
@@ -7041,13 +7107,13 @@ dependencies = [
[[package]]
name = "webview2-com-sys"
version = "0.31.0"
version = "0.33.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c76d5b77320ff155660be1df3e6588bc85c75f1a9feef938cc4dc4dd60d1d7cf"
checksum = "a3a3e2eeb58f82361c93f9777014668eb3d07e7d174ee4c819575a9208011886"
dependencies = [
"thiserror",
"windows 0.57.0",
"windows-core 0.57.0",
"windows 0.58.0",
"windows-core 0.58.0",
]
[[package]]
@@ -7103,9 +7169,9 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33082acd404763b315866e14a0d5193f3422c81086657583937a750cdd3ec340"
dependencies = [
"cocoa",
"cocoa 0.25.0",
"objc",
"raw-window-handle 0.6.2",
"raw-window-handle",
"windows-sys 0.52.0",
"windows-version",
]
@@ -7121,11 +7187,11 @@ dependencies = [
[[package]]
name = "windows"
version = "0.57.0"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143"
checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6"
dependencies = [
"windows-core 0.57.0",
"windows-core 0.58.0",
"windows-targets 0.52.6",
]
@@ -7140,21 +7206,22 @@ dependencies = [
[[package]]
name = "windows-core"
version = "0.57.0"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d"
checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99"
dependencies = [
"windows-implement",
"windows-interface",
"windows-result",
"windows-strings",
"windows-targets 0.52.6",
]
[[package]]
name = "windows-implement"
version = "0.57.0"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7"
checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b"
dependencies = [
"proc-macro2",
"quote",
@@ -7163,9 +7230,9 @@ dependencies = [
[[package]]
name = "windows-interface"
version = "0.57.0"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7"
checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515"
dependencies = [
"proc-macro2",
"quote",
@@ -7174,13 +7241,23 @@ dependencies = [
[[package]]
name = "windows-result"
version = "0.1.2"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8"
checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-strings"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
dependencies = [
"windows-result",
"windows-targets 0.52.6",
]
[[package]]
name = "windows-sys"
version = "0.45.0"
@@ -7425,14 +7502,14 @@ dependencies = [
[[package]]
name = "wry"
version = "0.41.0"
version = "0.43.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68b00c945786b02d7805d09a969fa36d0eee4e0bd4fb3ec2a79d2bf45a1b44cd"
checksum = "f4d715cf5fe88e9647f3d17b207b6d060d4a88e7171d4ccb2d2c657dd1d44728"
dependencies = [
"base64 0.22.1",
"block",
"cocoa",
"core-graphics",
"cocoa 0.26.0",
"core-graphics 0.24.0",
"crossbeam-channel",
"dpi",
"dunce",
@@ -7445,13 +7522,11 @@ dependencies = [
"kuchikiki",
"libc",
"ndk",
"ndk-context",
"ndk-sys",
"objc",
"objc_id",
"once_cell",
"percent-encoding",
"raw-window-handle 0.6.2",
"raw-window-handle",
"sha2",
"soup3",
"tao-macros",
@@ -7459,8 +7534,8 @@ dependencies = [
"webkit2gtk",
"webkit2gtk-sys",
"webview2-com",
"windows 0.57.0",
"windows-core 0.57.0",
"windows 0.58.0",
"windows-core 0.58.0",
"windows-version",
"x11-dl",
]
@@ -7540,7 +7615,7 @@ dependencies = [
"anyhow",
"base64 0.22.1",
"chrono",
"cocoa",
"cocoa 0.26.0",
"datetime",
"hex_color",
"http 1.1.0",
@@ -7568,6 +7643,7 @@ dependencies = [
"thiserror",
"tokio",
"tokio-stream",
"urlencoding",
"uuid",
"yaak_grpc",
"yaak_models",

View File

@@ -20,7 +20,7 @@ tauri-build = { version = "2.0.0-rc.0", features = [] }
[target.'cfg(target_os = "macos")'.dependencies]
objc = "0.2.7"
cocoa = "0.25.0"
cocoa = "0.26.0"
[target.'cfg(target_os = "linux")'.dependencies]
openssl-sys = { version = "0.9", features = ["vendored"] } # For Ubuntu installation to work
@@ -47,20 +47,21 @@ serde_yaml = "0.9.34"
tauri = { workspace = true }
tauri-plugin-shell = { workspace = true }
tauri-plugin-clipboard-manager = "2.1.0-beta.7"
tauri-plugin-dialog = "2.0.0-rc.0"
tauri-plugin-fs = "2.0.0-rc.0"
tauri-plugin-log = { version = "2.0.0-rc.0", features = ["colored"] }
tauri-plugin-os = "2.0.0-rc.0"
tauri-plugin-updater = "2.0.0-rc.0"
tauri-plugin-window-state = "2.0.0-rc.0"
tauri-plugin-dialog = "2.0.0-rc.5"
tauri-plugin-fs = "2.0.0-rc.3"
tauri-plugin-log = { version = "2.0.0-rc.2", features = ["colored"] }
tauri-plugin-os = "2.0.0-rc.1"
tauri-plugin-updater = "2.0.0-rc.3"
tauri-plugin-window-state = "2.0.0-rc.3"
tokio = { version = "1.36.0", features = ["sync"] }
tokio-stream = "0.1.15"
uuid = "1.7.0"
thiserror = "1.0.61"
mime_guess = "2.0.5"
urlencoding = "2.1.3"
[workspace.dependencies]
yaak_models = { path = "yaak_models" }
yaak_plugin_runtime = { path = "yaak_plugin_runtime" }
tauri = { version = "2.0.0-rc.2", features = ["devtools", "protocol-asset"] }
tauri-plugin-shell = "2.0.0-rc.0"
tauri-plugin-shell = "2.0.0-rc.3"
tauri = { version = "2.0.0-rc.10", features = ["devtools", "protocol-asset"] }

File diff suppressed because one or more lines are too long

View File

@@ -3320,6 +3320,13 @@
"core:webview:allow-create-webview-window"
]
},
{
"description": "core:webview:allow-get-all-webviews -> Enables the get_all_webviews command without any pre-configured scope.",
"type": "string",
"enum": [
"core:webview:allow-get-all-webviews"
]
},
{
"description": "core:webview:allow-internal-toggle-devtools -> Enables the internal_toggle_devtools command without any pre-configured scope.",
"type": "string",
@@ -3404,6 +3411,13 @@
"core:webview:deny-create-webview-window"
]
},
{
"description": "core:webview:deny-get-all-webviews -> Denies the get_all_webviews command without any pre-configured scope.",
"type": "string",
"enum": [
"core:webview:deny-get-all-webviews"
]
},
{
"description": "core:webview:deny-internal-toggle-devtools -> Denies the internal_toggle_devtools command without any pre-configured scope.",
"type": "string",
@@ -3530,6 +3544,13 @@
"core:window:allow-destroy"
]
},
{
"description": "core:window:allow-get-all-windows -> Enables the get_all_windows command without any pre-configured scope.",
"type": "string",
"enum": [
"core:window:allow-get-all-windows"
]
},
{
"description": "core:window:allow-hide -> Enables the hide command without any pre-configured scope.",
"type": "string",
@@ -3985,6 +4006,13 @@
"core:window:deny-destroy"
]
},
{
"description": "core:window:deny-get-all-windows -> Denies the get_all_windows command without any pre-configured scope.",
"type": "string",
"enum": [
"core:window:deny-get-all-windows"
]
},
{
"description": "core:window:deny-hide -> Denies the hide command without any pre-configured scope.",
"type": "string",

View File

@@ -3320,6 +3320,13 @@
"core:webview:allow-create-webview-window"
]
},
{
"description": "core:webview:allow-get-all-webviews -> Enables the get_all_webviews command without any pre-configured scope.",
"type": "string",
"enum": [
"core:webview:allow-get-all-webviews"
]
},
{
"description": "core:webview:allow-internal-toggle-devtools -> Enables the internal_toggle_devtools command without any pre-configured scope.",
"type": "string",
@@ -3404,6 +3411,13 @@
"core:webview:deny-create-webview-window"
]
},
{
"description": "core:webview:deny-get-all-webviews -> Denies the get_all_webviews command without any pre-configured scope.",
"type": "string",
"enum": [
"core:webview:deny-get-all-webviews"
]
},
{
"description": "core:webview:deny-internal-toggle-devtools -> Denies the internal_toggle_devtools command without any pre-configured scope.",
"type": "string",
@@ -3530,6 +3544,13 @@
"core:window:allow-destroy"
]
},
{
"description": "core:window:allow-get-all-windows -> Enables the get_all_windows command without any pre-configured scope.",
"type": "string",
"enum": [
"core:window:allow-get-all-windows"
]
},
{
"description": "core:window:allow-hide -> Enables the hide command without any pre-configured scope.",
"type": "string",
@@ -3985,6 +4006,13 @@
"core:window:deny-destroy"
]
},
{
"description": "core:window:deny-get-all-windows -> Denies the get_all_windows command without any pre-configured scope.",
"type": "string",
"enum": [
"core:window:deny-get-all-windows"
]
},
{
"description": "core:window:deny-hide -> Denies the hide command without any pre-configured scope.",
"type": "string",

View File

@@ -0,0 +1,12 @@
CREATE TABLE plugins
(
id TEXT NOT NULL
PRIMARY KEY,
model TEXT DEFAULT 'plugin' NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
checked_at DATETIME NULL,
enabled BOOLEAN NOT NULL,
directory TEXT NULL NOT NULL,
url TEXT NULL
);

View File

@@ -28,6 +28,7 @@ pub enum AnalyticsResource {
HttpRequest,
HttpResponse,
KeyValue,
Plugin,
Setting,
Sidebar,
Theme,

View File

@@ -23,7 +23,7 @@ use tauri::{Manager, Runtime, WebviewWindow};
use tokio::sync::oneshot;
use tokio::sync::watch::Receiver;
use yaak_models::models::{
Cookie, CookieJar, Environment, HttpRequest, HttpResponse, HttpResponseHeader,
Cookie, CookieJar, Environment, HttpRequest, HttpResponse, HttpResponseHeader, HttpUrlParameter,
};
use yaak_models::queries::{get_workspace, update_response_if_id, upsert_cookie_jar};
@@ -40,13 +40,8 @@ pub async fn send_http_request<R: Runtime>(
.expect("Failed to get Workspace");
let cb = &*window.app_handle().state::<PluginTemplateCallback>();
let cb = cb.for_send();
let rendered_request = render_http_request(
&request,
&workspace,
environment.as_ref(),
&cb,
)
.await;
let rendered_request =
render_http_request(&request, &workspace, environment.as_ref(), &cb).await;
let mut url_string = rendered_request.url;
@@ -101,6 +96,23 @@ pub async fn send_http_request<R: Runtime>(
let client = client_builder.build().expect("Failed to build client");
// Render query parameters
let mut query_params = Vec::new();
for p in rendered_request.url_parameters {
if !p.enabled || p.name.is_empty() {
continue;
}
// Replace path parameters with values from URL parameters
let old_url_string = url_string.clone();
url_string = replace_path_placeholder(&p, url_string.as_str());
// Treat as regular param if wasn't used as path param
if old_url_string == url_string {
query_params.push((p.name, p.value));
}
}
let uri = match http::Uri::from_str(url_string.as_str()) {
Ok(u) => u,
Err(e) => {
@@ -127,7 +139,7 @@ pub async fn send_http_request<R: Runtime>(
let m = Method::from_bytes(rendered_request.method.to_uppercase().as_bytes())
.expect("Failed to create method");
let mut request_builder = client.request(m, url);
let mut request_builder = client.request(m, url).query(&query_params);
let mut headers = HeaderMap::new();
headers.insert(USER_AGENT, HeaderValue::from_static("yaak"));
@@ -210,15 +222,6 @@ pub async fn send_http_request<R: Runtime>(
}
}
let mut query_params = Vec::new();
for p in rendered_request.url_parameters {
if !p.enabled || p.name.is_empty() {
continue;
}
query_params.push((p.name, p.value));
}
request_builder = request_builder.query(&query_params);
let request_body = rendered_request.body;
if let Some(body_type) = &rendered_request.body_type {
if request_body.contains_key("text") {
@@ -489,3 +492,119 @@ fn get_str_h<'a>(v: &'a HashMap<String, Value>, key: &str) -> &'a str {
Some(v) => v.as_str().unwrap_or_default(),
}
}
fn replace_path_placeholder(p: &HttpUrlParameter, url: &str) -> String {
if !p.enabled {
return url.to_string();
}
let re = regex::Regex::new(format!("(/){}([/?#]|$)", p.name).as_str()).unwrap();
let result = re
.replace_all(url, |cap: &regex::Captures| {
format!(
"{}{}{}",
cap[1].to_string(),
urlencoding::encode(p.value.as_str()),
cap[2].to_string()
)
})
.into_owned();
result
}
#[cfg(test)]
mod tests {
use crate::http_request::replace_path_placeholder;
use yaak_models::models::HttpUrlParameter;
#[test]
fn placeholder_middle() {
let p = HttpUrlParameter {
name: ":foo".into(),
value: "xxx".into(),
enabled: true,
};
assert_eq!(
replace_path_placeholder(&p, "https://example.com/:foo/bar"),
"https://example.com/xxx/bar",
);
}
#[test]
fn placeholder_end() {
let p = HttpUrlParameter {
name: ":foo".into(),
value: "xxx".into(),
enabled: true,
};
assert_eq!(
replace_path_placeholder(&p, "https://example.com/:foo"),
"https://example.com/xxx",
);
}
#[test]
fn placeholder_query() {
let p = HttpUrlParameter {
name: ":foo".into(),
value: "xxx".into(),
enabled: true,
};
assert_eq!(
replace_path_placeholder(&p, "https://example.com/:foo?:foo"),
"https://example.com/xxx?:foo",
);
}
#[test]
fn placeholder_missing() {
let p = HttpUrlParameter {
enabled: true,
name: "".to_string(),
value: "".to_string(),
};
assert_eq!(
replace_path_placeholder(&p, "https://example.com/:missing"),
"https://example.com/:missing",
);
}
#[test]
fn placeholder_disabled() {
let p = HttpUrlParameter {
enabled: false,
name: ":foo".to_string(),
value: "xxx".to_string(),
};
assert_eq!(
replace_path_placeholder(&p, "https://example.com/:foo"),
"https://example.com/:foo",
);
}
#[test]
fn placeholder_prefix() {
let p = HttpUrlParameter {
name: ":foo".into(),
value: "xxx".into(),
enabled: true,
};
assert_eq!(
replace_path_placeholder(&p, "https://example.com/:foooo"),
"https://example.com/:foooo",
);
}
#[test]
fn placeholder_encode() {
let p = HttpUrlParameter {
name: ":foo".into(),
value: "Hello World".into(),
enabled: true,
};
assert_eq!(
replace_path_placeholder(&p, "https://example.com/:foo"),
"https://example.com/Hello%20World",
);
}
}

View File

@@ -1,6 +1,5 @@
extern crate core;
#[cfg(target_os = "macos")]
#[macro_use]
extern crate objc;
use std::collections::HashMap;
@@ -41,7 +40,7 @@ use crate::updates::{UpdateMode, YaakUpdater};
use crate::window_menu::app_menu;
use yaak_models::models::{
CookieJar, Environment, EnvironmentVariable, Folder, GrpcConnection, GrpcEvent, GrpcEventType,
GrpcRequest, HttpRequest, HttpResponse, KeyValue, ModelType, Settings, Workspace,
GrpcRequest, HttpRequest, HttpResponse, KeyValue, ModelType, Plugin, Settings, Workspace,
};
use yaak_models::queries::{
cancel_pending_grpc_connections, cancel_pending_responses, create_default_http_response,
@@ -50,16 +49,17 @@ use yaak_models::queries::{
delete_http_response, delete_workspace, duplicate_grpc_request, duplicate_http_request,
generate_model_id, get_cookie_jar, get_environment, get_folder, get_grpc_connection,
get_grpc_request, get_http_request, get_http_response, get_key_value_raw,
get_or_create_settings, get_workspace, list_cookie_jars, list_environments, list_folders,
list_grpc_connections, list_grpc_events, list_grpc_requests, list_http_requests,
list_http_responses, list_workspaces, set_key_value_raw, update_response_if_id,
get_or_create_settings, get_plugin, get_workspace, list_cookie_jars, list_environments,
list_folders, list_grpc_connections, list_grpc_events, list_grpc_requests, list_http_requests,
list_http_responses, list_plugins, list_workspaces, set_key_value_raw, update_response_if_id,
update_settings, upsert_cookie_jar, upsert_environment, upsert_folder, upsert_grpc_connection,
upsert_grpc_event, upsert_grpc_request, upsert_http_request, upsert_workspace,
upsert_grpc_event, upsert_grpc_request, upsert_http_request, upsert_plugin, upsert_workspace,
};
use yaak_plugin_runtime::events::{
CallHttpRequestActionRequest, FilterResponse, FindHttpResponsesResponse,
GetHttpRequestActionsResponse, GetHttpRequestByIdResponse, GetTemplateFunctionsResponse,
InternalEvent, InternalEventPayload, RenderHttpRequestResponse, SendHttpRequestResponse,
InternalEvent, InternalEventPayload, PluginBootResponse, RenderHttpRequestResponse,
SendHttpRequestResponse,
};
use yaak_templates::{Parser, Tokens};
@@ -78,6 +78,9 @@ mod window_menu;
const DEFAULT_WINDOW_WIDTH: f64 = 1100.0;
const DEFAULT_WINDOW_HEIGHT: f64 = 600.0;
const MIN_WINDOW_WIDTH: f64 = 300.0;
const MIN_WINDOW_HEIGHT: f64 = 300.0;
#[derive(serde::Serialize)]
#[serde(default, rename_all = "camelCase")]
struct AppMetaData {
@@ -842,16 +845,33 @@ async fn cmd_import_data(
imported_resources.environments.len()
);
for mut v in resources.folders {
v.id = maybe_gen_id(v.id.as_str(), ModelType::TypeFolder, &mut id_map);
v.workspace_id = maybe_gen_id(
v.workspace_id.as_str(),
ModelType::TypeWorkspace,
&mut id_map,
);
v.folder_id = maybe_gen_id_opt(v.folder_id, ModelType::TypeFolder, &mut id_map);
let x = upsert_folder(&w, v).await.map_err(|e| e.to_string())?;
imported_resources.folders.push(x.clone());
// Folders can foreign-key to themselves, so we need to import from
// the top of the tree to the bottom to avoid foreign key conflicts.
// We do this by looping until we've imported them all, only importing if:
// - The parent folder has been imported
// - The folder hasn't already been imported
// The loop exits when imported.len == to_import.len
while imported_resources.folders.len() < resources.folders.len() {
for mut v in resources.folders.clone() {
v.id = maybe_gen_id(v.id.as_str(), ModelType::TypeFolder, &mut id_map);
v.workspace_id = maybe_gen_id(
v.workspace_id.as_str(),
ModelType::TypeWorkspace,
&mut id_map,
);
v.folder_id = maybe_gen_id_opt(v.folder_id, ModelType::TypeFolder, &mut id_map);
if let Some(fid) = v.folder_id.clone() {
let imported_parent = imported_resources.folders.iter().find(|f| f.id == fid);
if imported_parent.is_none() {
continue;
}
}
if let Some(_) = imported_resources.folders.iter().find(|f| f.id == v.id) {
continue;
}
let x = upsert_folder(&w, v).await.map_err(|e| e.to_string())?;
imported_resources.folders.push(x.clone());
}
}
info!("Imported {} folders", imported_resources.folders.len());
@@ -1149,6 +1169,20 @@ async fn cmd_create_workspace(name: &str, w: WebviewWindow) -> Result<Workspace,
.map_err(|e| e.to_string())
}
#[tauri::command]
async fn cmd_create_plugin(directory: &str, url: Option<String>, w: WebviewWindow) -> Result<Plugin, String> {
upsert_plugin(
&w,
Plugin {
directory: directory.into(),
url,
..Default::default()
},
)
.await
.map_err(|e| e.to_string())
}
#[tauri::command]
async fn cmd_update_cookie_jar(
cookie_jar: CookieJar,
@@ -1392,10 +1426,9 @@ async fn cmd_list_grpc_requests(
workspace_id: &str,
w: WebviewWindow,
) -> Result<Vec<GrpcRequest>, String> {
let requests = list_grpc_requests(&w, workspace_id)
list_grpc_requests(&w, workspace_id)
.await
.map_err(|e| e.to_string())?;
Ok(requests)
.map_err(|e| e.to_string())
}
#[tauri::command]
@@ -1403,11 +1436,9 @@ async fn cmd_list_http_requests(
workspace_id: &str,
w: WebviewWindow,
) -> Result<Vec<HttpRequest>, String> {
let requests = list_http_requests(&w, workspace_id)
list_http_requests(&w, workspace_id)
.await
.expect("Failed to find requests");
// .map_err(|e| e.to_string())
Ok(requests)
.map_err(|e| e.to_string())
}
#[tauri::command]
@@ -1415,11 +1446,27 @@ async fn cmd_list_environments(
workspace_id: &str,
w: WebviewWindow,
) -> Result<Vec<Environment>, String> {
let environments = list_environments(&w, workspace_id)
list_environments(&w, workspace_id)
.await
.expect("Failed to find environments");
.map_err(|e| e.to_string())
}
Ok(environments)
#[tauri::command]
async fn cmd_list_plugins(w: WebviewWindow) -> Result<Vec<Plugin>, String> {
list_plugins(&w).await.map_err(|e| e.to_string())
}
#[tauri::command]
async fn cmd_plugin_info(
id: &str,
w: WebviewWindow,
plugin_manager: State<'_, PluginManager>,
) -> Result<PluginBootResponse, String> {
let plugin = get_plugin(&w, id).await.map_err(|e| e.to_string())?;
plugin_manager
.get_plugin_info(plugin.directory.as_str())
.await
.ok_or("Failed to find plugin info".to_string())
}
#[tauri::command]
@@ -1673,6 +1720,7 @@ pub fn run() {
cmd_create_folder,
cmd_create_grpc_request,
cmd_create_http_request,
cmd_create_plugin,
cmd_create_workspace,
cmd_curl_to_request,
cmd_delete_all_grpc_connections,
@@ -1714,6 +1762,8 @@ pub fn run() {
cmd_list_grpc_requests,
cmd_list_http_requests,
cmd_list_http_responses,
cmd_list_plugins,
cmd_plugin_info,
cmd_list_workspaces,
cmd_metadata,
cmd_new_nested_window,
@@ -1815,6 +1865,7 @@ fn create_nested_window(
.title(title)
.parent(&window)
.unwrap()
.min_inner_size(MIN_WINDOW_WIDTH, MIN_WINDOW_HEIGHT)
.inner_size(DEFAULT_WINDOW_WIDTH * 0.7, DEFAULT_WINDOW_HEIGHT * 0.9);
// Add macOS-only things
@@ -1825,7 +1876,7 @@ fn create_nested_window(
.title_bar_style(TitleBarStyle::Overlay);
}
// Add non-MacOS things
// Add non-macOS things
#[cfg(not(target_os = "macos"))]
{
win_builder = win_builder.decorations(false);
@@ -1858,7 +1909,7 @@ fn create_window(handle: &AppHandle, url: &str) -> WebviewWindow {
100.0 + random::<f64>() * 30.0,
100.0 + random::<f64>() * 30.0,
)
.min_inner_size(300.0, 300.0)
.min_inner_size(MIN_WINDOW_WIDTH, MIN_WINDOW_HEIGHT)
.title(handle.package_info().name.to_string());
// Add macOS-only things
@@ -1884,7 +1935,8 @@ fn create_window(handle: &AppHandle, url: &str) -> WebviewWindow {
return;
}
match event.id().0.as_str() {
let event_id = event.id().0.as_str();
match event_id {
"quit" => exit(0),
"close" => w.close().unwrap(),
"zoom_reset" => w.emit("zoom_reset", true).unwrap(),
@@ -1955,7 +2007,7 @@ fn monitor_plugin_events<R: Runtime>(app_handle: &AppHandle<R>) {
}
async fn handle_plugin_event<R: Runtime>(app_handle: &AppHandle<R>, event: &InternalEvent) {
info!("Got event to app {}", event.id);
// info!("Got event to app {}", event.id);
let response_event: Option<InternalEventPayload> = match event.clone().payload {
InternalEventPayload::CopyTextRequest(req) => {
app_handle

View File

@@ -10,8 +10,9 @@ use yaak_plugin_runtime::manager::PluginManager;
use crate::is_dev;
// Check for updates every 3 hours
const MAX_UPDATE_CHECK_SECONDS: u64 = 60 * 60 * 3;
const MAX_UPDATE_CHECK_HOURS_STABLE: u64 = 12;
const MAX_UPDATE_CHECK_HOURS_BETA: u64 = 3;
const MAX_UPDATE_CHECK_HOURS_ALPHA: u64 = 1;
// Create updater struct
pub struct YaakUpdater {
@@ -129,8 +130,13 @@ impl YaakUpdater {
app_handle: &AppHandle,
mode: UpdateMode,
) -> Result<bool, tauri_plugin_updater::Error> {
let ignore_check =
self.last_update_check.elapsed().unwrap().as_secs() < MAX_UPDATE_CHECK_SECONDS;
let update_period_seconds = match mode {
UpdateMode::Stable => MAX_UPDATE_CHECK_HOURS_STABLE,
UpdateMode::Beta => MAX_UPDATE_CHECK_HOURS_BETA,
UpdateMode::Alpha => MAX_UPDATE_CHECK_HOURS_ALPHA,
} * (60 * 60);
let seconds_since_last_check = self.last_update_check.elapsed().unwrap().as_secs();
let ignore_check = seconds_since_last_check < update_period_seconds;
if ignore_check {
return Ok(false);
}

View File

@@ -2,9 +2,9 @@ use thiserror::Error;
#[derive(Error, Debug)]
pub enum Error {
#[error("SQL error")]
#[error("SQL error: {0}")]
SqlError(#[from] rusqlite::Error),
#[error("JSON error")]
#[error("JSON error: {0}")]
JsonError(#[from] serde_json::Error),
#[error("unknown error")]
Unknown,

View File

@@ -20,7 +20,7 @@ pub struct Settings {
pub theme_light: String,
pub update_channel: String,
pub interface_font_size: i32,
pub interface_scale: i32,
pub interface_scale: f32,
pub editor_font_size: i32,
pub editor_soft_wrap: bool,
pub telemetry: bool,
@@ -655,6 +655,7 @@ pub struct GrpcEvent {
pub request_id: String,
pub connection_id: String,
pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime,
pub content: String,
pub event_type: GrpcEventType,
pub metadata: HashMap<String, String>,
@@ -693,6 +694,7 @@ impl<'s> TryFrom<&Row<'s>> for GrpcEvent {
request_id: r.get("request_id")?,
connection_id: r.get("connection_id")?,
created_at: r.get("created_at")?,
updated_at: r.get("updated_at")?,
content: r.get("content")?,
event_type: serde_json::from_str(event_type.as_str()).unwrap_or_default(),
metadata: serde_json::from_str(metadata.as_str()).unwrap_or_default(),
@@ -702,6 +704,51 @@ impl<'s> TryFrom<&Row<'s>> for GrpcEvent {
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
#[serde(default, rename_all = "camelCase")]
pub struct Plugin {
pub id: String,
#[ts(type = "\"plugin\"")]
pub model: String,
pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime,
pub checked_at: Option<NaiveDateTime>,
pub directory: String,
pub url: Option<String>,
pub enabled: bool,
}
#[derive(Iden)]
pub enum PluginIden {
#[iden = "plugins"]
Table,
Id,
Model,
CreatedAt,
UpdatedAt,
CheckedAt,
Directory,
Url,
Enabled,
}
impl<'s> TryFrom<&Row<'s>> for Plugin {
type Error = rusqlite::Error;
fn try_from(r: &Row<'s>) -> Result<Self, Self::Error> {
Ok(Plugin {
id: r.get("id")?,
model: r.get("model")?,
created_at: r.get("created_at")?,
updated_at: r.get("updated_at")?,
checked_at: r.get("checked_at")?,
url: r.get("url")?,
directory: r.get("directory")?,
enabled: r.get("enabled")?,
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
#[serde(default, rename_all = "camelCase")]
pub struct KeyValue {
@@ -758,6 +805,7 @@ pub enum ModelType {
TypeGrpcRequest,
TypeHttpRequest,
TypeHttpResponse,
TypePlugin,
TypeWorkspace,
}
@@ -772,6 +820,7 @@ impl ModelType {
ModelType::TypeGrpcRequest => "gr",
ModelType::TypeHttpRequest => "rq",
ModelType::TypeHttpResponse => "rs",
ModelType::TypePlugin => "pg",
ModelType::TypeWorkspace => "wk",
}
.to_string()

View File

@@ -1,12 +1,7 @@
use std::fs;
use crate::error::Result;
use crate::models::{
CookieJar, CookieJarIden, Environment, EnvironmentIden, Folder, FolderIden, GrpcConnection,
GrpcConnectionIden, GrpcEvent, GrpcEventIden, GrpcRequest, GrpcRequestIden, HttpRequest,
HttpRequestIden, HttpResponse, HttpResponseHeader, HttpResponseIden, KeyValue, KeyValueIden,
ModelType, Settings, SettingsIden, Workspace, WorkspaceIden,
};
use crate::models::{CookieJar, CookieJarIden, Environment, EnvironmentIden, Folder, FolderIden, GrpcConnection, GrpcConnectionIden, GrpcEvent, GrpcEventIden, GrpcRequest, GrpcRequestIden, HttpRequest, HttpRequestIden, HttpResponse, HttpResponseHeader, HttpResponseIden, KeyValue, KeyValueIden, ModelType, Plugin, PluginIden, Settings, SettingsIden, Workspace, WorkspaceIden};
use crate::plugin::SqliteConnection;
use log::{debug, error};
use rand::distributions::{Alphanumeric, DistString};
@@ -848,7 +843,7 @@ pub async fn upsert_environment<R: Runtime>(
serde_json::to_string(&environment.variables)?.into(),
])
.on_conflict(
OnConflict::column(GrpcEventIden::Id)
OnConflict::column(EnvironmentIden::Id)
.update_columns([
EnvironmentIden::UpdatedAt,
EnvironmentIden::Name,
@@ -877,6 +872,88 @@ pub async fn get_environment<R: Runtime>(mgr: &impl Manager<R>, id: &str) -> Res
Ok(stmt.query_row(&*params.as_params(), |row| row.try_into())?)
}
pub async fn get_plugin<R: Runtime>(
mgr: &impl Manager<R>,
id: &str
) -> Result<Plugin> {
let dbm = &*mgr.state::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap();
let (sql, params) = Query::select()
.from(PluginIden::Table)
.column(Asterisk)
.cond_where(Expr::col(EnvironmentIden::Id).eq(id))
.build_rusqlite(SqliteQueryBuilder);
let mut stmt = db.prepare(sql.as_str())?;
Ok(stmt.query_row(&*params.as_params(), |row| row.try_into())?)
}
pub async fn list_plugins<R: Runtime>(
mgr: &impl Manager<R>,
) -> Result<Vec<Plugin>> {
let dbm = &*mgr.state::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap();
let (sql, params) = Query::select()
.from(PluginIden::Table)
.column(Asterisk)
.order_by(PluginIden::CreatedAt, Order::Desc)
.build_rusqlite(SqliteQueryBuilder);
let mut stmt = db.prepare(sql.as_str())?;
let items = stmt.query_map(&*params.as_params(), |row| row.try_into())?;
Ok(items.map(|v| v.unwrap()).collect())
}
pub async fn upsert_plugin<R: Runtime>(
window: &WebviewWindow<R>,
plugin: Plugin,
) -> Result<Plugin> {
let id = match plugin.id.as_str() {
"" => generate_model_id(ModelType::TypePlugin),
_ => plugin.id.to_string(),
};
let dbm = &*window.app_handle().state::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap();
let (sql, params) = Query::insert()
.into_table(PluginIden::Table)
.columns([
PluginIden::Id,
PluginIden::CreatedAt,
PluginIden::UpdatedAt,
PluginIden::CheckedAt,
PluginIden::Directory,
PluginIden::Url,
PluginIden::Enabled,
])
.values_panic([
id.as_str().into(),
CurrentTimestamp.into(),
CurrentTimestamp.into(),
plugin.checked_at.into(),
plugin.directory.into(),
plugin.url.into(),
plugin.enabled.into(),
])
.on_conflict(
OnConflict::column(PluginIden::Id)
.update_columns([
PluginIden::UpdatedAt,
PluginIden::CheckedAt,
PluginIden::Directory,
PluginIden::Url,
PluginIden::Enabled,
])
.to_owned(),
)
.returning_all()
.build_rusqlite(SqliteQueryBuilder);
let mut stmt = db.prepare(sql.as_str())?;
let m = stmt.query_row(&*params.as_params(), |row| row.try_into())?;
Ok(emit_upserted_model(window, m))
}
pub async fn get_folder<R: Runtime>(mgr: &impl Manager<R>, id: &str) -> Result<Folder> {
let dbm = &*mgr.state::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap();

View File

@@ -2,10 +2,7 @@ use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use ts_rs::TS;
use yaak_models::models::{
CookieJar, Environment, Folder, GrpcConnection, GrpcEvent, GrpcRequest, HttpRequest,
HttpResponse, KeyValue, Settings, Workspace,
};
use yaak_models::models::{CookieJar, Environment, Folder, GrpcConnection, GrpcEvent, GrpcRequest, HttpRequest, HttpResponse, KeyValue, Plugin, Settings, Workspace};
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase")]
@@ -21,8 +18,8 @@ pub struct InternalEvent {
#[serde(rename_all = "snake_case", tag = "type")]
#[ts(export)]
pub enum InternalEventPayload {
BootRequest(BootRequest),
BootResponse(BootResponse),
BootRequest(PluginBootRequest),
BootResponse(PluginBootResponse),
ImportRequest(ImportRequest),
ImportResponse(ImportResponse),
@@ -71,14 +68,14 @@ pub struct EmptyResponse {}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export)]
pub struct BootRequest {
pub struct PluginBootRequest {
pub dir: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export)]
pub struct BootResponse {
pub struct PluginBootResponse {
pub name: String,
pub version: String,
pub capabilities: Vec<String>,
@@ -401,4 +398,5 @@ pub enum Model {
Workspace(Workspace),
CookieJar(CookieJar),
Settings(Settings),
Plugin(Plugin),
}

View File

@@ -1,9 +1,9 @@
use crate::error::Result;
use crate::events::{
CallHttpRequestActionRequest, CallTemplateFunctionArgs, RenderPurpose,
PluginBootResponse, CallHttpRequestActionRequest, CallTemplateFunctionArgs,
CallTemplateFunctionRequest, CallTemplateFunctionResponse, FilterRequest, FilterResponse,
GetHttpRequestActionsRequest, GetHttpRequestActionsResponse, GetTemplateFunctionsResponse,
ImportRequest, ImportResponse, InternalEvent, InternalEventPayload,
ImportRequest, ImportResponse, InternalEvent, InternalEventPayload, RenderPurpose,
};
use std::collections::HashMap;
@@ -64,6 +64,10 @@ impl PluginManager {
.await
}
pub async fn get_plugin_info(&self, dir: &str) -> Option<PluginBootResponse> {
self.server.plugin_by_dir(dir).await.ok()?.info().await
}
pub async fn get_http_request_actions(&self) -> Result<Vec<GetHttpRequestActionsResponse>> {
let reply_events = self
.server
@@ -155,7 +159,9 @@ impl PluginManager {
});
match result {
None => Err(PluginErr("No importers found for file contents".to_string())),
None => Err(PluginErr(
"No importers found for file contents".to_string(),
)),
Some((resp, ref_id)) => {
let plugin = self.server.plugin_by_ref_id(ref_id.as_str()).await?;
let plugin_name = plugin.name().await;

View File

@@ -15,21 +15,30 @@ use tokio::fs::read_dir;
use tokio::net::TcpListener;
use tonic::codegen::tokio_stream;
use tonic::transport::Server;
use yaak_models::queries::list_plugins;
pub fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::new("yaak_plugin_runtime")
.setup(|app, _| {
let plugins_dir = app
.setup(|app_handle, _| {
let plugins_dir = app_handle
.path()
.resolve("plugins", BaseDirectory::Resource)
.expect("failed to resolve plugin directory resource");
tauri::async_runtime::block_on(async move {
let plugin_dirs = read_plugins_dir(&plugins_dir)
let bundled_plugin_dirs = read_plugins_dir(&plugins_dir)
.await
.expect("Failed to read plugins dir");
let manager = PluginManager::new(&app, plugin_dirs).await;
app.manage(manager);
.expect(format!("Failed to read plugins dir: {:?}", plugins_dir).as_str());
let plugins = list_plugins(app_handle).await.unwrap_or_default();
let installed_plugin_dirs = plugins
.iter()
.map(|p| p.directory.to_owned())
.collect::<Vec<String>>();
let plugin_dirs = [installed_plugin_dirs, bundled_plugin_dirs].concat();
let manager = PluginManager::new(&app_handle, plugin_dirs).await;
app_handle.manage(manager);
Ok(())
})
})

View File

@@ -1,4 +1,3 @@
use log::info;
use rand::distributions::{Alphanumeric, DistString};
use std::collections::HashMap;
use std::pin::Pin;
@@ -11,7 +10,7 @@ use tonic::{Request, Response, Status, Streaming};
use crate::error::Error::PluginNotFoundErr;
use crate::error::Result;
use crate::events::{BootRequest, BootResponse, InternalEvent, InternalEventPayload};
use crate::events::{PluginBootRequest, PluginBootResponse, InternalEvent, InternalEventPayload};
use crate::server::plugin_runtime::plugin_runtime_server::PluginRuntime;
use plugin_runtime::EventStreamEvent;
use yaak_models::queries::generate_id;
@@ -28,7 +27,7 @@ pub struct PluginHandle {
dir: String,
to_plugin_tx: Arc<Mutex<mpsc::Sender<tonic::Result<EventStreamEvent>>>>,
ref_id: String,
boot_resp: Arc<Mutex<Option<BootResponse>>>,
boot_resp: Arc<Mutex<Option<PluginBootResponse>>>,
}
impl PluginHandle {
@@ -38,6 +37,11 @@ impl PluginHandle {
Some(r) => r.name.to_owned(),
}
}
pub async fn info(&self) -> Option<PluginBootResponse> {
let resp = &*self.boot_resp.lock().await;
resp.clone()
}
pub fn build_event_to_send(
&self,
@@ -53,11 +57,11 @@ impl PluginHandle {
}
pub async fn send(&self, event: &InternalEvent) -> Result<()> {
info!(
"Sending event to plugin {} {:?}",
event.id,
self.name().await
);
// info!(
// "Sending event to plugin {} {:?}",
// event.id,
// self.name().await
// );
self.to_plugin_tx
.lock()
.await
@@ -68,7 +72,7 @@ impl PluginHandle {
Ok(())
}
pub async fn boot(&self, resp: &BootResponse) {
pub async fn boot(&self, resp: &PluginBootResponse) {
let mut boot_resp = self.boot_resp.lock().await;
*boot_resp = Some(resp.clone());
}
@@ -120,7 +124,7 @@ impl PluginRuntimeGrpcServer {
};
}
pub async fn boot_plugin(&self, id: &str, resp: &BootResponse) {
pub async fn boot_plugin(&self, id: &str, resp: &PluginBootResponse) {
match self.plugin_ref_to_plugin.lock().await.get(id) {
None => {
println!("Tried booting non-existing plugin {}", id);
@@ -151,61 +155,27 @@ impl PluginRuntimeGrpcServer {
plugin_handle
}
// pub async fn callback(
// &self,
// source_event: InternalEvent,
// payload: InternalEventPayload,
// ) -> Result<InternalEvent> {
// let reply_id = match source_event.clone().reply_id {
// None => {
// let msg = format!("Source event missing reply Id {:?}", source_event.clone());
// return Err(MissingCallbackIdErr(msg));
// }
// Some(id) => id,
// };
//
// let callbacks = self.callbacks.lock().await;
// let plugin_name = match callbacks.get(reply_id.as_str()) {
// None => {
// let msg = format!("Callback not found {:?}", source_event);
// return Err(MissingCallbackErr(msg));
// }
// Some(n) => n,
// };
//
// let plugins = self.plugins.lock().await;
// let plugin = match plugins.get(plugin_name) {
// None => {
// let msg = format!(
// "Plugin not found {plugin_name}. Choices were {:?}",
// plugins.keys()
// );
// return Err(UnknownPluginErr(msg));
// }
// Some(n) => n,
// };
//
// plugin.send(&payload, Some(reply_id)).await
// }
pub async fn plugin_by_ref_id(&self, ref_id: &str) -> Result<PluginHandle> {
let plugins = self.plugin_ref_to_plugin.lock().await;
if plugins.is_empty() {
return Err(PluginNotFoundErr(ref_id.into()));
}
match plugins.get(ref_id) {
None => Err(PluginNotFoundErr(ref_id.into())),
Some(p) => Ok(p.to_owned()),
}
}
pub async fn plugin_by_dir(&self, dir: &str) -> Result<PluginHandle> {
let plugins = self.plugin_ref_to_plugin.lock().await;
for p in plugins.values() {
if p.dir == dir {
return Ok(p.to_owned());
}
}
Err(PluginNotFoundErr(dir.into()))
}
pub async fn plugin_by_name(&self, plugin_name: &str) -> Result<PluginHandle> {
let plugins = self.plugin_ref_to_plugin.lock().await;
if plugins.is_empty() {
return Err(PluginNotFoundErr(plugin_name.into()));
}
for p in plugins.values() {
if p.name().await == plugin_name {
return Ok(p.to_owned());
@@ -341,7 +311,7 @@ impl PluginRuntimeGrpcServer {
plugin_ids.push(plugin.clone().ref_id);
let event = plugin.build_event_to_send(
&InternalEventPayload::BootRequest(BootRequest {
&InternalEventPayload::BootRequest(PluginBootRequest {
dir: dir.to_string(),
}),
None,

View File

@@ -1,3 +1,4 @@
import { useActiveRequest } from '../hooks/useActiveRequest';
import { useCreateDropdownItems } from '../hooks/useCreateDropdownItems';
import type { DropdownProps } from './core/Dropdown';
import { Dropdown } from './core/Dropdown';
@@ -7,7 +8,13 @@ interface Props extends Omit<DropdownProps, 'items'> {
}
export function CreateDropdown({ hideFolder, children, ...props }: Props) {
const items = useCreateDropdownItems({ hideFolder, hideIcons: true });
const activeRequest = useActiveRequest();
const folderId = activeRequest?.folderId ?? null;
const items = useCreateDropdownItems({
hideFolder,
hideIcons: true,
folderId,
});
return (
<Dropdown items={items} {...props}>
{children}

View File

@@ -18,6 +18,7 @@ import { httpResponsesQueryKey } from '../hooks/useHttpResponses';
import { keyValueQueryKey } from '../hooks/useKeyValue';
import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
import { useNotificationToast } from '../hooks/useNotificationToast';
import { pluginsAtom } from '../hooks/usePlugins';
import { useRecentCookieJars } from '../hooks/useRecentCookieJars';
import { useRecentEnvironments } from '../hooks/useRecentEnvironments';
import { useRecentRequests } from '../hooks/useRecentRequests';
@@ -67,6 +68,7 @@ export function GlobalHooks() {
const setSettings = useSetAtom(settingsAtom);
const setWorkspaces = useSetAtom(workspacesAtom);
const setPlugins = useSetAtom(pluginsAtom);
const setHttpRequests = useSetAtom(httpRequestsAtom);
const setGrpcRequests = useSetAtom(grpcRequestsAtom);
const setEnvironments = useSetAtom(environmentsAtom);
@@ -100,6 +102,8 @@ export function GlobalHooks() {
if (model.model === 'workspace') {
setWorkspaces(updateModelList(model, pushToFront));
} else if (model.model === 'plugin') {
setPlugins(updateModelList(model, pushToFront));
} else if (model.model === 'http_request') {
setHttpRequests(updateModelList(model, pushToFront));
} else if (model.model === 'grpc_request') {
@@ -129,6 +133,8 @@ export function GlobalHooks() {
if (model.model === 'workspace') {
setWorkspaces(removeById(model));
} else if (model.model === 'plugin') {
setPlugins(removeById(model));
} else if (model.model === 'http_request') {
setHttpRequests(removeById(model));
} else if (model.model === 'http_response') {
@@ -162,15 +168,16 @@ export function GlobalHooks() {
document.documentElement.style.setProperty('--editor-font-size', `${editorFontSize}px`);
}, [settings]);
// Handle Zoom. Note, Mac handles it in app menu, so need to also handle keyboard
// Handle Zoom.
// Note, Mac handles it in the app menu, so need to also handle keyboard
// shortcuts for Windows/Linux
const zoom = useZoom();
useHotKey('app.zoom_in', () => zoom.zoomIn);
useListenToTauriEvent('zoom_in', () => zoom.zoomIn);
useHotKey('app.zoom_out', () => zoom.zoomOut);
useListenToTauriEvent('zoom_out', () => zoom.zoomOut);
useHotKey('app.zoom_reset', () => zoom.zoomReset);
useListenToTauriEvent('zoom_reset', () => zoom.zoomReset);
useHotKey('app.zoom_in', zoom.zoomIn);
useListenToTauriEvent('zoom_in', zoom.zoomIn);
useHotKey('app.zoom_out', zoom.zoomOut);
useListenToTauriEvent('zoom_out', zoom.zoomOut);
useHotKey('app.zoom_reset', zoom.zoomReset);
useListenToTauriEvent('zoom_reset', zoom.zoomReset);
const copy = useCopy();
useListenToTauriEvent('generate_theme_css', () => {

View File

@@ -128,7 +128,7 @@ export function GraphQLEditor({ defaultValue, onChange, baseRequest, ...extraEdi
return (
<div className="h-full w-full grid grid-cols-1 grid-rows-[minmax(0,100%)_auto]">
<Editor
contentType="application/graphql"
language="graphql"
defaultValue={query ?? ''}
format={formatGraphQL}
heightMode="auto"
@@ -144,7 +144,7 @@ export function GraphQLEditor({ defaultValue, onChange, baseRequest, ...extraEdi
</Separator>
<Editor
format={tryFormatJson}
contentType="application/json"
language="json"
defaultValue={JSON.stringify(variables, null, 2)}
heightMode="auto"
onChange={handleChangeVariables}

View File

@@ -185,7 +185,7 @@ export function GrpcEditor({
return (
<div className="h-full w-full grid grid-cols-1 grid-rows-[minmax(0,100%)_auto_auto_minmax(0,auto)]">
<Editor
contentType="application/json"
language="json"
autocompleteVariables
useTemplating
forceUpdateKey={request.id}

View File

@@ -45,10 +45,9 @@ export function GrpcProtoSelection({ requestId }: Props) {
multiple: true,
filters: [{ name: 'Proto Files', extensions: ['proto'] }],
});
if (selected == null) {
return;
}
const newFiles = selected.map((f) => f.path).filter((p) => !protoFiles.includes(p));
if (selected == null) return;
const newFiles = selected.filter((p) => !protoFiles.includes(p));
await protoFilesKv.set([...protoFiles, ...newFiles]);
await grpc.reflect.refetch();
}}

View File

@@ -1,4 +1,5 @@
import type { HttpResponse } from '@yaakapp/api';
import { useCopyHttpResponse } from '../hooks/useCopyHttpResponse';
import { useDeleteHttpResponse } from '../hooks/useDeleteHttpResponse';
import { useDeleteHttpResponses } from '../hooks/useDeleteHttpResponses';
import { useSaveResponse } from '../hooks/useSaveResponse';
@@ -25,6 +26,7 @@ export const RecentResponsesDropdown = function ResponsePane({
const deleteAllResponses = useDeleteHttpResponses(activeResponse?.requestId);
const latestResponseId = responses[0]?.id ?? 'n/a';
const saveResponse = useSaveResponse(activeResponse);
const copyResponse = useCopyHttpResponse(activeResponse);
return (
<Dropdown
@@ -37,6 +39,14 @@ export const RecentResponsesDropdown = function ResponsePane({
hidden: responses.length === 0,
disabled: responses.length === 0,
},
{
key: 'copy',
label: 'Copy to Clipboard',
onSelect: copyResponse.mutate,
leftSlot: <Icon icon="copy" />,
hidden: responses.length === 0,
disabled: responses.length === 0,
},
{
key: 'clear-single',
label: 'Delete',

View File

@@ -8,10 +8,12 @@ import { useContentTypeFromHeaders } from '../hooks/useContentTypeFromHeaders';
import { useImportCurl } from '../hooks/useImportCurl';
import { useIsResponseLoading } from '../hooks/useIsResponseLoading';
import { usePinnedHttpResponse } from '../hooks/usePinnedHttpResponse';
import { useRequestEditorEvent } from '../hooks/useRequestEditor';
import { useRequests } from '../hooks/useRequests';
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
import { useSendAnyHttpRequest } from '../hooks/useSendAnyHttpRequest';
import { useUpdateAnyHttpRequest } from '../hooks/useUpdateAnyHttpRequest';
import { languageFromContentType } from '../lib/contentType';
import { tryFormatJson } from '../lib/formatters';
import {
AUTH_TYPE_BASIC,
@@ -33,6 +35,7 @@ import { CountBadge } from './core/CountBadge';
import { Editor } from './core/Editor';
import type { GenericCompletionOption } from './core/Editor/genericCompletion';
import { InlineCode } from './core/InlineCode';
import type { Pair } from './core/PairEditor';
import type { TabItem } from './core/Tabs/Tabs';
import { TabContent, Tabs } from './core/Tabs/Tabs';
import { EmptyStateText } from './EmptyStateText';
@@ -51,7 +54,14 @@ interface Props {
activeRequest: HttpRequest;
}
const useActiveTab = createGlobalState<string>('body');
const useActiveTab = createGlobalState<Record<string, string>>({});
const TAB_BODY = 'body';
const TAB_PARAMS = 'params';
const TAB_HEADERS = 'headers';
const TAB_AUTH = 'auth';
const DEFAULT_TAB = TAB_BODY;
export const RequestPane = memo(function RequestPane({
style,
@@ -62,7 +72,7 @@ export const RequestPane = memo(function RequestPane({
const requests = useRequests();
const activeRequestId = activeRequest.id;
const updateRequest = useUpdateAnyHttpRequest();
const [activeTab, setActiveTab] = useActiveTab();
const [activeTabs, setActiveTabs] = useActiveTab();
const [forceUpdateHeaderEditorKey, setForceUpdateHeaderEditorKey] = useState<number>(0);
const { updateKey: forceUpdateKey } = useRequestUpdateKey(activeRequest.id ?? null);
const contentType = useContentTypeFromHeaders(activeRequest.headers);
@@ -88,10 +98,27 @@ export const RequestPane = memo(function RequestPane({
const toast = useToast();
const { urlParameterPairs, urlParametersKey } = useMemo(() => {
const placeholderNames = Array.from(activeRequest.url.matchAll(/\/(:[^/]+)/g)).map(
(m) => m[1] ?? '',
);
const nonEmptyParameters = activeRequest.urlParameters.filter((p) => p.name || p.value);
const items: Pair[] = [...nonEmptyParameters];
for (const name of placeholderNames) {
const index = items.findIndex((p) => p.name === name);
if (index >= 0) {
items[index]!.readOnlyName = true;
} else {
items.push({ name, value: '', enabled: true, readOnlyName: true });
}
}
return { urlParameterPairs: items, urlParametersKey: placeholderNames.join(',') };
}, [activeRequest.url, activeRequest.urlParameters]);
const tabs: TabItem[] = useMemo(
() => [
{
value: 'body',
value: TAB_BODY,
options: {
value: activeRequest.bodyType,
items: [
@@ -157,16 +184,16 @@ export const RequestPane = memo(function RequestPane({
},
},
{
value: 'params',
value: TAB_PARAMS,
label: (
<div className="flex items-center">
Params
<CountBadge count={activeRequest.urlParameters.filter((p) => p.name).length} />
<CountBadge count={urlParameterPairs.length} />
</div>
),
},
{
value: 'headers',
value: TAB_HEADERS,
label: (
<div className="flex items-center">
Headers
@@ -175,7 +202,7 @@ export const RequestPane = memo(function RequestPane({
),
},
{
value: 'auth',
value: TAB_AUTH,
label: 'Auth',
options: {
value: activeRequest.authenticationType,
@@ -211,11 +238,11 @@ export const RequestPane = memo(function RequestPane({
activeRequest.bodyType,
activeRequest.headers,
activeRequest.method,
activeRequest.urlParameters,
activeRequestId,
handleContentTypeChange,
toast,
updateRequest,
urlParameterPairs,
],
);
@@ -269,6 +296,18 @@ export const RequestPane = memo(function RequestPane({
const { updateKey } = useRequestUpdateKey(activeRequestId ?? null);
const importCurl = useImportCurl();
const activeTab = activeTabs[activeRequestId] ?? DEFAULT_TAB;
const setActiveTab = useCallback(
(tab: string) => {
setActiveTabs((r) => ({ ...r, [activeRequest.id]: tab }));
},
[activeRequest.id, setActiveTabs],
);
useRequestEditorEvent('request_pane.focus_tab', () => {
setActiveTab(TAB_PARAMS);
});
return (
<div
style={style}
@@ -315,13 +354,14 @@ export const RequestPane = memo(function RequestPane({
isLoading={isLoading}
/>
<Tabs
key={activeRequest.id} // Freshen tabs on request change
value={activeTab}
label="Request"
onChangeValue={setActiveTab}
tabs={tabs}
tabListClassName="mt-2 !mb-1.5"
>
<TabContent value="auth">
<TabContent value={TAB_AUTH}>
{activeRequest.authenticationType === AUTH_TYPE_BASIC ? (
<BasicAuth key={forceUpdateKey} request={activeRequest} />
) : activeRequest.authenticationType === AUTH_TYPE_BEARER ? (
@@ -332,21 +372,21 @@ export const RequestPane = memo(function RequestPane({
</EmptyStateText>
)}
</TabContent>
<TabContent value="headers">
<TabContent value={TAB_HEADERS}>
<HeadersEditor
forceUpdateKey={`${forceUpdateHeaderEditorKey}::${forceUpdateKey}`}
headers={activeRequest.headers}
onChange={handleHeadersChange}
/>
</TabContent>
<TabContent value="params">
<TabContent value={TAB_PARAMS}>
<UrlParametersEditor
forceUpdateKey={forceUpdateKey}
urlParameters={activeRequest.urlParameters}
forceUpdateKey={forceUpdateKey + urlParametersKey}
pairs={urlParameterPairs}
onChange={handleUrlParametersChange}
/>
</TabContent>
<TabContent value="body">
<TabContent value={TAB_BODY}>
{activeRequest.bodyType === BODY_TYPE_JSON ? (
<Editor
forceUpdateKey={forceUpdateKey}
@@ -355,7 +395,7 @@ export const RequestPane = memo(function RequestPane({
placeholder="..."
heightMode={fullHeight ? 'full' : 'auto'}
defaultValue={`${activeRequest.body?.text ?? ''}`}
contentType="application/json"
language="json"
onChange={handleBodyTextChange}
format={tryFormatJson}
/>
@@ -367,7 +407,7 @@ export const RequestPane = memo(function RequestPane({
placeholder="..."
heightMode={fullHeight ? 'full' : 'auto'}
defaultValue={`${activeRequest.body?.text ?? ''}`}
contentType="text/xml"
language="xml"
onChange={handleBodyTextChange}
/>
) : activeRequest.bodyType === BODY_TYPE_GRAPHQL ? (
@@ -402,6 +442,7 @@ export const RequestPane = memo(function RequestPane({
forceUpdateKey={forceUpdateKey}
useTemplating
autocompleteVariables
language={languageFromContentType(contentType)}
placeholder="..."
heightMode={fullHeight ? 'full' : 'auto'}
defaultValue={`${activeRequest.body?.text ?? ''}`}

View File

@@ -1,11 +1,11 @@
import type { HttpRequest } from '@yaakapp/api';
import classNames from 'classnames';
import type { CSSProperties } from 'react';
import { memo, useMemo } from 'react';
import { memo, useCallback, useMemo } from 'react';
import { createGlobalState } from 'react-use';
import { useContentTypeFromHeaders } from '../hooks/useContentTypeFromHeaders';
import { usePinnedHttpResponse } from '../hooks/usePinnedHttpResponse';
import { useResponseViewMode } from '../hooks/useResponseViewMode';
import type { HttpRequest } from '@yaakapp/api';
import { isResponseLoading } from '../lib/models';
import { Banner } from './core/Banner';
import { CountBadge } from './core/CountBadge';
@@ -23,11 +23,10 @@ import { ResponseHeaders } from './ResponseHeaders';
import { ResponseInfo } from './ResponseInfo';
import { AudioViewer } from './responseViewers/AudioViewer';
import { CsvViewer } from './responseViewers/CsvViewer';
import { HTMLOrTextViewer } from './responseViewers/HTMLOrTextViewer';
import { ImageViewer } from './responseViewers/ImageViewer';
import { PdfViewer } from './responseViewers/PdfViewer';
import { TextViewer } from './responseViewers/TextViewer';
import { VideoViewer } from './responseViewers/VideoViewer';
import { WebPageViewer } from './responseViewers/WebPageViewer';
interface Props {
style?: CSSProperties;
@@ -35,18 +34,30 @@ interface Props {
activeRequest: HttpRequest;
}
const useActiveTab = createGlobalState<string>('body');
const useActiveTab = createGlobalState<Record<string, string>>({});
const TAB_BODY = 'body';
const TAB_HEADERS = 'headers';
const TAB_INFO = 'info';
const DEFAULT_TAB = TAB_BODY;
export const ResponsePane = memo(function ResponsePane({ style, className, activeRequest }: Props) {
const { activeResponse, setPinnedResponseId, responses } = usePinnedHttpResponse(activeRequest);
const [viewMode, setViewMode] = useResponseViewMode(activeResponse?.requestId);
const [activeTab, setActiveTab] = useActiveTab();
const [activeTabs, setActiveTabs] = useActiveTab();
const contentType = useContentTypeFromHeaders(activeResponse?.headers ?? null);
const activeTab = activeTabs[activeRequest.id] ?? DEFAULT_TAB;
const setActiveTab = useCallback(
(tab: string) => {
setActiveTabs((r) => ({ ...r, [activeRequest.id]: tab }));
},
[activeRequest.id, setActiveTabs],
);
const tabs = useMemo<TabItem[]>(
() => [
{
value: 'body',
value: TAB_BODY,
label: 'Preview Mode',
options: {
value: viewMode,
@@ -58,6 +69,7 @@ export const ResponsePane = memo(function ResponsePane({ style, className, activ
},
},
{
value: TAB_HEADERS,
label: (
<div className="flex items-center">
Headers
@@ -66,11 +78,10 @@ export const ResponsePane = memo(function ResponsePane({ style, className, activ
/>
</div>
),
value: 'headers',
},
{
value: TAB_INFO,
label: 'Info',
value: 'info',
},
],
[activeResponse?.headers, contentType, setViewMode, viewMode],
@@ -143,20 +154,15 @@ export const ResponsePane = memo(function ResponsePane({ style, className, activ
</Banner>
) : (
<Tabs
key={activeRequest.id} // Freshen tabs on request change
value={activeTab}
onChangeValue={setActiveTab}
label="Response"
tabs={tabs}
label="Response"
className="ml-3 mr-3 mb-3"
tabListClassName="mt-1.5"
>
<TabContent value="headers">
<ResponseHeaders response={activeResponse} />
</TabContent>
<TabContent value="info">
<ResponseInfo response={activeResponse} />
</TabContent>
<TabContent value="body">
<TabContent value={TAB_BODY}>
{!activeResponse.contentLength ? (
<div className="pb-2 h-full">
<EmptyStateText>Empty Body</EmptyStateText>
@@ -171,18 +177,22 @@ export const ResponsePane = memo(function ResponsePane({ style, className, activ
<PdfViewer response={activeResponse} />
) : contentType?.match(/csv|tab-separated/) ? (
<CsvViewer className="pb-2" response={activeResponse} />
) : viewMode === 'pretty' && contentType?.includes('html') ? (
<WebPageViewer response={activeResponse} />
) : (
// ) : viewMode === 'pretty' && contentType?.includes('json') ? (
// <JsonAttributeTree attrValue={activeResponse} />
<TextViewer
className="-mr-2 bg-surface" // Pull to the right
<HTMLOrTextViewer
textViewerClassName="-mr-2 bg-surface" // Pull to the right
response={activeResponse}
pretty={viewMode === 'pretty'}
/>
)}
</TabContent>
<TabContent value={TAB_HEADERS}>
<ResponseHeaders response={activeResponse} />
</TabContent>
<TabContent value={TAB_INFO}>
<ResponseInfo response={activeResponse} />
</TabContent>
</Tabs>
)}
</div>

View File

@@ -9,22 +9,32 @@ import { HStack } from './core/Stacks';
type Props = ButtonProps & {
onChange: (value: { filePath: string | null; contentType: string | null }) => void;
filePath: string | null;
directory?: boolean;
inline?: boolean;
noun?: string;
};
// Special character to insert ltr text in rtl element
const rtlEscapeChar = <>&#x200E;</>;
export function SelectFile({ onChange, filePath, inline, className }: Props) {
export function SelectFile({
onChange,
filePath,
inline,
className,
directory,
noun,
size = 'sm',
...props
}: Props) {
const handleClick = async () => {
const selected = await open({
const filePath = await open({
title: 'Select File',
multiple: false,
directory,
});
if (selected == null) return;
const filePath = selected.path;
const contentType = typeof filePath === 'string' && filePath ? mime.getType(filePath) : null;
if (filePath == null) return;
const contentType = filePath ? mime.getType(filePath) : null;
onChange({ filePath, contentType });
};
@@ -32,31 +42,41 @@ export function SelectFile({ onChange, filePath, inline, className }: Props) {
onChange({ filePath: null, contentType: null });
};
const itemLabel = noun ?? (directory ? 'Folder' : 'File');
const selectOrChange = (filePath ? 'Change ' : 'Select ') + itemLabel;
return (
<HStack space={1.5} className="group relative justify-stretch">
<HStack space={1.5} className="group relative justify-stretch overflow-hidden">
<Button
className={classNames(className, 'font-mono text-xs rtl', inline && 'w-full')}
color="secondary"
size="sm"
onClick={handleClick}
size={size}
{...props}
>
{rtlEscapeChar}
{inline ? <>{filePath || 'Select File'}</> : <>Select File</>}
{inline ? filePath || selectOrChange : selectOrChange}
</Button>
{!inline && (
<>
{filePath && (
<IconButton
size="sm"
size={size}
variant="border"
icon="x"
title="Unset File"
title={'Unset ' + itemLabel}
onClick={handleClear}
/>
)}
<div className="text-sm font-mono truncate rtl pr-3 text">
<div
className={classNames(
'font-mono truncate rtl pl-1.5 pr-3 text-text',
size === 'xs' && 'text-xs',
size === 'sm' && 'text-sm',
)}
>
{rtlEscapeChar}
{filePath ?? 'No file selected'}
{filePath ?? `No ${itemLabel.toLowerCase()} selected`}
</div>
</>
)}

View File

@@ -10,13 +10,15 @@ import { HeaderSize } from '../HeaderSize';
import { WindowControls } from '../WindowControls';
import { SettingsAppearance } from './SettingsAppearance';
import { SettingsGeneral } from './SettingsGeneral';
import { SettingsPlugins } from './SettingsPlugins';
enum Tab {
General = 'general',
Appearance = 'appearance',
Plugins = 'plugins',
}
const tabs = [Tab.General, Tab.Appearance];
const tabs = [Tab.General, Tab.Appearance, Tab.Plugins];
export const Settings = () => {
const osInfo = useOsInfo();
@@ -58,6 +60,9 @@ export const Settings = () => {
<TabContent value={Tab.Appearance} className="pt-3 overflow-y-auto h-full px-4">
<SettingsAppearance />
</TabContent>
<TabContent value={Tab.Plugins} className="pt-3 overflow-y-auto h-full px-4">
<SettingsPlugins />
</TabContent>
</Tabs>
</div>
);

View File

@@ -198,7 +198,7 @@ export function SettingsAppearance() {
'};',
].join('\n')}
heightMode="auto"
contentType="application/javascript"
language="javascript"
/>
</VStack>
</VStack>

View File

@@ -117,7 +117,7 @@ export function SettingsDesign() {
'};',
].join('\n')}
heightMode="auto"
contentType="application/javascript"
language="javascript"
/>
<div className="flex flex-col gap-1">
<div className="flex flex-wrap gap-1">

View File

@@ -0,0 +1,95 @@
import type { Plugin } from '@yaakapp/api';
import React from 'react';
import { useCreatePlugin } from '../../hooks/useCreatePlugin';
import { usePluginInfo } from '../../hooks/usePluginInfo';
import { usePlugins, useRefreshPlugins } from '../../hooks/usePlugins';
import { Button } from '../core/Button';
import { Checkbox } from '../core/Checkbox';
import { IconButton } from '../core/IconButton';
import { InlineCode } from '../core/InlineCode';
import { HStack } from '../core/Stacks';
import { SelectFile } from '../SelectFile';
export function SettingsPlugins() {
const [directory, setDirectory] = React.useState<string | null>(null);
const plugins = usePlugins();
const createPlugin = useCreatePlugin();
const refreshPlugins = useRefreshPlugins();
return (
<div className="grid grid-rows-[minmax(0,1fr)_auto] h-full">
<table className="w-full text-sm mb-auto min-w-full max-w-full divide-y divide-surface-highlight">
<thead>
<tr>
<th></th>
<th className="py-2 text-left">Plugin</th>
<th className="py-2 text-right">Version</th>
<th></th>
</tr>
</thead>
<tbody className="divide-y divide-surface-highlight">
{plugins.map((p) => (
<PluginInfo key={p.id} plugin={p} />
))}
</tbody>
</table>
<form
onSubmit={(e) => {
e.preventDefault();
if (directory == null) return;
createPlugin.mutate(directory);
setDirectory(null);
}}
>
<footer className="grid grid-cols-[minmax(0,1fr)_auto] -mx-4 py-2 px-4 border-t bg-surface-highlight border-border-subtle min-w-0">
<SelectFile
size="xs"
noun="Plugin"
directory
onChange={({ filePath }) => setDirectory(filePath)}
filePath={directory}
/>
<HStack>
{directory && (
<Button size="xs" type="submit" color="primary" className="ml-auto">
Add Plugin
</Button>
)}
<IconButton
size="sm"
icon="refresh"
title="Reload plugins"
spin={refreshPlugins.isPending}
onClick={() => refreshPlugins.mutate()}
/>
</HStack>
</footer>
</form>
</div>
);
}
function PluginInfo({ plugin }: { plugin: Plugin }) {
const pluginInfo = usePluginInfo(plugin.id);
if (pluginInfo.data == null) return null;
return (
<tr className="group">
<td className="pr-2">
<Checkbox hideLabel checked={true} title="foo" onChange={() => null} />
</td>
<td className="py-2 select-text cursor-text w-full">
<InlineCode>{pluginInfo.data?.name}</InlineCode>
</td>
<td className="py-2 select-text cursor-text text-right">
<InlineCode>{pluginInfo.data?.version}</InlineCode>
</td>
<td className="py-2 select-text cursor-text pl-2">
<IconButton
size="sm"
icon="trash"
title="Uninstall plugin"
className="text-text-subtlest"
/>
</td>
</tr>
);
}

View File

@@ -67,7 +67,7 @@ export const UrlBar = memo(function UrlBar({
wrapLines={isFocused}
hideLabel
useTemplating
contentType="url"
language="url"
className="pl-0 pr-1.5 py-0.5"
name="url"
label="Enter URL"

View File

@@ -1,23 +1,45 @@
import type { HttpRequest } from '@yaakapp/api';
import { useRequestEditorEvent } from '../hooks/useRequestEditor';
import type { PairEditorRef } from './core/PairEditor';
import { PairOrBulkEditor } from './core/PairOrBulkEditor';
import { VStack } from './core/Stacks';
import { useRef } from 'react';
type Props = {
forceUpdateKey: string;
urlParameters: HttpRequest['headers'];
pairs: HttpRequest['headers'];
onChange: (headers: HttpRequest['urlParameters']) => void;
};
export function UrlParametersEditor({ urlParameters, forceUpdateKey, onChange }: Props) {
export function UrlParametersEditor({ pairs, forceUpdateKey, onChange }: Props) {
const pairEditor = useRef<PairEditorRef>(null);
useRequestEditorEvent(
'request_params.focus_value',
(name) => {
const pairIndex = pairs.findIndex((p) => p.name === name);
if (pairIndex >= 0) {
pairEditor.current?.focusValue(pairIndex);
} else {
console.log("Couldn't find pair to focus", { name, pairs });
}
},
[pairs],
);
return (
<PairOrBulkEditor
preferenceName="url_parameters"
valueAutocompleteVariables
nameAutocompleteVariables
namePlaceholder="param_name"
valuePlaceholder="Value"
pairs={urlParameters}
onChange={onChange}
forceUpdateKey={forceUpdateKey}
/>
<VStack className="h-full">
<PairOrBulkEditor
ref={pairEditor}
preferenceName="url_parameters"
valueAutocompleteVariables
nameAutocompleteVariables
namePlaceholder="param_name"
valuePlaceholder="Value"
pairs={pairs}
onChange={onChange}
forceUpdateKey={forceUpdateKey}
/>
</VStack>
);
}

View File

@@ -57,7 +57,7 @@ export function WindowControls({ className, onlyX }: Props) {
)}
<Button
color="custom"
className="!h-full px-4 text-text-subtle rounded-none hocus:bg-fg-danger hocus:text"
className="!h-full px-4 text-text-subtle rounded-none hocus:bg-danger hocus:text"
onClick={() => getCurrentWebviewWindow().close()}
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">

View File

@@ -14,8 +14,9 @@ export function Banner({ children, className, color = 'secondary' }: Props) {
className={classNames(
className,
`x-theme-banner--${color}`,
'border border-dashed italic px-3 py-2 rounded select-auto cursor-text',
'border-border-subtle bg-surface-highlight text',
'border border-dashed border-border-subtle bg-surface-highlight',
'italic px-3 py-2 rounded select-auto cursor-text',
'overflow-x-auto text-text',
)}
>
{children}

View File

@@ -36,18 +36,19 @@ export function BulkPairEditor({
forceUpdateKey={forceUpdateKey}
placeholder={`${namePlaceholder ?? 'name'}: ${valuePlaceholder ?? 'value'}`}
defaultValue={pairsText}
contentType="pairs"
language="pairs"
onChange={handleChange}
/>
);
}
function lineToPair(l: string): PairEditorProps['pairs'][0] {
const [name, ...values] = l.split(':');
function lineToPair(line: string): PairEditorProps['pairs'][0] {
const [, name, value] = line.match(/^(:?[^:]+):\s+([^$]*)/) ?? [];
const pair: PairEditorProps['pairs'][0] = {
enabled: true,
name: (name ?? '').trim(),
value: values.join(':').trim(),
value: (value ?? '').trim(),
};
return pair;
}

View File

@@ -73,7 +73,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
size === 'md' && 'h-md px-3 rounded-md',
size === 'sm' && 'h-sm px-2.5 rounded-md',
size === 'xs' && 'h-xs px-2 text-sm rounded-md',
size === '2xs' && 'h-5 px-1 text-xs rounded',
size === '2xs' && 'h-2xs px-1 text-xs rounded',
// Solids
variant === 'solid' && 'border-transparent',

View File

@@ -19,7 +19,8 @@
}
.cm-line {
@apply w-full; /* Important! Ensure it spans the entire width */
@apply w-full;
/* Important! Ensure it spans the entire width */
@apply w-full text-text pl-1 pr-1.5;
}
@@ -169,9 +170,14 @@
}
}
.cm-wrapper.cm-readonly .cm-editor {
.cm-cursor {
@apply border-danger !important;
/* Cursor and mouse cursor for readonly mode */
.cm-wrapper.cm-readonly {
.cm-editor .cm-cursor {
@apply hidden !important;
}
&.cm-singleline .cm-line {
@apply cursor-default;
}
}

View File

@@ -19,6 +19,7 @@ import {
} from 'react';
import { useActiveEnvironmentVariables } from '../../../hooks/useActiveEnvironmentVariables';
import { parseTemplate } from '../../../hooks/useParseTemplate';
import { useRequestEditor } from '../../../hooks/useRequestEditor';
import { useSettings } from '../../../hooks/useSettings';
import { useTemplateFunctions } from '../../../hooks/useTemplateFunctions';
import { useDialog } from '../../DialogContext';
@@ -39,10 +40,11 @@ export { formatSdl } from 'format-graphql';
export interface EditorProps {
id?: string;
readOnly?: boolean;
disabled?: boolean;
type?: 'text' | 'password';
className?: string;
heightMode?: 'auto' | 'full';
contentType?: string | null;
language?: 'javascript' | 'json' | 'html' | 'xml' | 'graphql' | 'url' | 'pairs' | 'text';
forceUpdateKey?: string | number;
autoFocus?: boolean;
autoSelect?: boolean;
@@ -71,7 +73,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
readOnly,
type = 'text',
heightMode,
contentType,
language = 'text',
autoFocus,
autoSelect,
placeholder,
@@ -226,12 +228,20 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
[dialog],
);
// Update language extension when contentType changes
const { focusParamValue } = useRequestEditor();
const onClickPathParameter = useCallback(
async (name: string) => {
focusParamValue(name);
},
[focusParamValue],
);
// Update the language extension when the language changes
useEffect(() => {
if (cm.current === null) return;
const { view, languageCompartment } = cm.current;
const ext = getLanguageExtension({
contentType,
language,
environmentVariables,
useTemplating,
autocomplete,
@@ -239,10 +249,11 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
onClickFunction,
onClickVariable,
onClickMissingVariable,
onClickPathParameter,
});
view.dispatch({ effects: languageCompartment.reconfigure(ext) });
}, [
contentType,
language,
autocomplete,
useTemplating,
environmentVariables,
@@ -250,6 +261,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
onClickFunction,
onClickVariable,
onClickMissingVariable,
onClickPathParameter,
]);
// Initialize the editor when ref mounts
@@ -265,7 +277,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
try {
const languageCompartment = new Compartment();
const langExt = getLanguageExtension({
contentType,
language,
useTemplating,
autocomplete,
environmentVariables,
@@ -273,6 +285,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
onClickVariable,
onClickFunction,
onClickMissingVariable,
onClickPathParameter,
});
const state = EditorState.create({

View File

@@ -32,7 +32,7 @@ import {
} from '@codemirror/view';
import { tags as t } from '@lezer/highlight';
import type { EnvironmentVariable, TemplateFunction } from '@yaakapp/api';
import { graphql, graphqlLanguageSupport } from 'cm6-graphql';
import { graphql } from 'cm6-graphql';
import { EditorView } from 'codemirror';
import type { EditorProps } from './index';
import { pairs } from './pairs/extension';
@@ -46,6 +46,10 @@ export const syntaxHighlightStyle = HighlightStyle.define([
color: 'var(--textSubtlest)',
fontStyle: 'italic',
},
{
tag: [t.emphasis],
textDecoration: 'underline',
},
{
tag: [t.paren, t.bracket, t.brace],
color: 'var(--textSubtle)',
@@ -64,19 +68,19 @@ export const syntaxHighlightStyle = HighlightStyle.define([
const syntaxTheme = EditorView.theme({}, { dark: true });
const syntaxExtensions: Record<string, LanguageSupport> = {
'application/graphql': graphqlLanguageSupport(),
'application/json': json(),
'application/javascript': javascript(),
'text/html': xml(), // HTML as XML because HTML is oddly slow
'application/xml': xml(),
'text/xml': xml(),
const syntaxExtensions: Record<NonNullable<EditorProps['language']>, LanguageSupport | null> = {
graphql: null,
json: json(),
javascript: javascript(),
html: xml(), // HTML as XML because HTML is oddly slow
xml: xml(),
url: url(),
pairs: pairs(),
text: text(),
};
export function getLanguageExtension({
contentType,
language,
useTemplating = false,
environmentVariables,
autocomplete,
@@ -84,18 +88,20 @@ export function getLanguageExtension({
onClickVariable,
onClickFunction,
onClickMissingVariable,
onClickPathParameter,
}: {
environmentVariables: EnvironmentVariable[];
templateFunctions: TemplateFunction[];
onClickFunction: (option: TemplateFunction, tagValue: string, startPos: number) => void;
onClickVariable: (option: EnvironmentVariable, tagValue: string, startPos: number) => void;
onClickMissingVariable: (name: string, tagValue: string, startPos: number) => void;
} & Pick<EditorProps, 'contentType' | 'useTemplating' | 'autocomplete'>) {
const justContentType = contentType?.split(';')[0] ?? contentType ?? '';
if (justContentType === 'application/graphql') {
onClickPathParameter: (name: string) => void;
} & Pick<EditorProps, 'language' | 'useTemplating' | 'autocomplete'>) {
if (language === 'graphql') {
return graphql();
}
const base = syntaxExtensions[justContentType] ?? text();
const base = syntaxExtensions[language ?? 'text'] ?? text();
if (!useTemplating) {
return base;
}
@@ -108,6 +114,7 @@ export function getLanguageExtension({
onClickFunction,
onClickVariable,
onClickMissingVariable,
onClickPathParameter,
});
}

View File

@@ -1,8 +1,8 @@
@top pairs { (Key? Sep Value)* }
@top pairs { (Key Sep Value "\n")* }
@tokens {
Sep { ":" }
Key { ![:]+ }
Key { ":"? ![:]+ }
Value { ![\n]+ }
}

View File

@@ -0,0 +1,6 @@
// This file was generated by lezer-generator. You probably shouldn't edit it.
export const
pairs = 1,
Key = 2,
Sep = 3,
Value = 4

View File

@@ -3,17 +3,17 @@ import {LRParser} from "@lezer/lr"
import {highlight} from "./highlight"
export const parser = LRParser.deserialize({
version: 14,
states: "!QQQOPOOOYOQO'#CaO_OPO'#CaQQOPOOOOOO,58{,58{OdOQO,58{OOOO-E6_-E6_OOOO1G.g1G.g",
stateData: "i~OQQORPO~OSSO~ORTO~OSVO~O",
goto: "]UPPPPPVQRORUR",
states: "zQQOPOOOVOQO'#CaQQOPOOO[OSO,58{OOOO-E6_-E6_OaOQO1G.gOOOO7+$R7+$R",
stateData: "f~OQPO~ORRO~OSTO~OVUO~O",
goto: "]UPPPPPVQQORSQ",
nodeNames: "⚠ pairs Key Sep Value",
maxTerm: 6,
maxTerm: 7,
propSources: [highlight],
skippedNodes: [0],
repeatNodeCount: 1,
tokenData: "#oRRVOYhYZ!UZ![h![!]#[!];'Sh;'S;=`#U<%lOhRoVQPSQOYhYZ!UZ![h![!]!m!];'Sh;'S;=`#U<%lOhP!ZSQPO![!U!];'S!U;'S;=`!g<%lO!UP!jP;=`<%l!UQ!rSSQOY!mZ;'S!m;'S;=`#O<%lO!mQ#RP;=`<%l!mR#XP;=`<%lhR#cSRPSQOY!mZ;'S!m;'S;=`#O<%lO!m",
tokenizers: [0, 1],
tokenData: "$]VRVOYhYZ#[Z![h![!]#o!];'Sh;'S;=`#U<%lOhToVQPSSOYhYZ!UZ![h![!]!m!];'Sh;'S;=`#U<%lOhP!ZSQPO![!U!];'S!U;'S;=`!g<%lO!UP!jP;=`<%l!US!rSSSOY!mZ;'S!m;'S;=`#O<%lO!mS#RP;=`<%l!mT#XP;=`<%lhR#cSVQQPO![!U!];'S!U;'S;=`!g<%lO!UV#vVRQSSOYhYZ!UZ![h![!]!m!];'Sh;'S;=`#U<%lOh",
tokenizers: [0, 1, 2],
topRules: {"pairs":[0,1]},
tokenPrec: 0
tokenPrec: 0,
termNames: {"0":"⚠","1":"@top","2":"Key","3":"Sep","4":"Value","5":"(Key Sep Value \"\\n\")+","6":"␄","7":"\"\\n\""}
})

View File

@@ -18,6 +18,7 @@ export function twig({
onClickFunction,
onClickVariable,
onClickMissingVariable,
onClickPathParameter,
}: {
base: LanguageSupport;
environmentVariables: EnvironmentVariable[];
@@ -26,6 +27,7 @@ export function twig({
onClickFunction: (option: TemplateFunction, tagValue: string, startPos: number) => void;
onClickVariable: (option: EnvironmentVariable, tagValue: string, startPos: number) => void;
onClickMissingVariable: (name: string, tagValue: string, startPos: number) => void;
onClickPathParameter: (name: string) => void;
}) {
const language = mixLanguage(base);
@@ -62,11 +64,11 @@ export function twig({
return [
language,
base.support,
templateTagsPlugin(options, onClickMissingVariable),
language.data.of({ autocomplete: completions }),
base.language.data.of({ autocomplete: completions }),
language.data.of({ autocomplete: genericCompletion(autocomplete) }),
base.language.data.of({ autocomplete: genericCompletion(autocomplete) }),
templateTagsPlugin(options, onClickMissingVariable, onClickPathParameter),
];
}

View File

@@ -2,9 +2,42 @@ import { syntaxTree } from '@codemirror/language';
import type { Range } from '@codemirror/state';
import type { DecorationSet, ViewUpdate } from '@codemirror/view';
import { Decoration, ViewPlugin, WidgetType } from '@codemirror/view';
import type { SyntaxNodeRef } from '@lezer/common';
import { EditorView } from 'codemirror';
import type { TwigCompletionOption } from './completion';
class PathPlaceholderWidget extends WidgetType {
readonly #clickListenerCallback: () => void;
constructor(readonly rawText: string, readonly startPos: number, readonly onClick: () => void) {
super();
this.#clickListenerCallback = () => {
this.onClick?.();
};
}
eq(other: PathPlaceholderWidget) {
return this.startPos === other.startPos && this.rawText === other.rawText;
}
toDOM() {
const elt = document.createElement('span');
elt.className = `x-theme-templateTag x-theme-templateTag--secondary template-tag`;
elt.textContent = this.rawText;
elt.addEventListener('click', this.#clickListenerCallback);
return elt;
}
destroy(dom: HTMLElement) {
dom.removeEventListener('click', this.#clickListenerCallback);
super.destroy(dom);
}
ignoreEvent() {
return false;
}
}
class TemplateTagWidget extends WidgetType {
readonly #clickListenerCallback: () => void;
@@ -62,20 +95,40 @@ function templateTags(
view: EditorView,
options: TwigCompletionOption[],
onClickMissingVariable: (name: string, rawTag: string, startPos: number) => void,
onClickPathParameter: (name: string) => void,
): DecorationSet {
const widgets: Range<Decoration>[] = [];
for (const { from, to } of view.visibleRanges) {
syntaxTree(view.state).iterate({
const tree = syntaxTree(view.state);
tree.iterate({
from,
to,
enter(node) {
if (node.name == 'Tag') {
// Don't decorate if the cursor is inside the match
for (const r of view.state.selection.ranges) {
if (r.from > node.from && r.to < node.to) {
return;
if (node.name === 'Text') {
// Find the `url` node and then jump into it to find the placeholders
for (let i = node.from; i < node.to; i++) {
const innerTree = syntaxTree(view.state).resolveInner(i);
if (innerTree.node.name === 'url') {
innerTree.toTree().iterate({
enter(node) {
if (node.name !== 'Placeholder') return;
if (isSelectionInsideNode(view, node)) return;
const globalFrom = innerTree.node.from + node.from;
const globalTo = innerTree.node.from + node.to;
const rawText = view.state.doc.sliceString(globalFrom, globalTo);
const onClick = () => onClickPathParameter(rawText);
const widget = new PathPlaceholderWidget(rawText, globalFrom, onClick);
const deco = Decoration.replace({ widget, inclusive: false });
widgets.push(deco.range(globalFrom, globalTo));
},
});
break;
}
}
} else if (node.name === 'Tag') {
// Don't decorate if the cursor is inside the match
if (isSelectionInsideNode(view, node)) return;
const rawTag = view.state.doc.sliceString(node.from, node.to);
@@ -114,17 +167,28 @@ function templateTags(
export function templateTagsPlugin(
options: TwigCompletionOption[],
onClickMissingVariable: (name: string, tagValue: string, startPos: number) => void,
onClickPathParameter: (name: string) => void,
) {
return ViewPlugin.fromClass(
class {
decorations: DecorationSet;
constructor(view: EditorView) {
this.decorations = templateTags(view, options, onClickMissingVariable);
this.decorations = templateTags(
view,
options,
onClickMissingVariable,
onClickPathParameter,
);
}
update(update: ViewUpdate) {
this.decorations = templateTags(update.view, options, onClickMissingVariable);
this.decorations = templateTags(
update.view,
options,
onClickMissingVariable,
onClickPathParameter,
);
}
},
{
@@ -136,13 +200,13 @@ export function templateTagsPlugin(
return view.plugin(plugin)?.decorations || Decoration.none;
});
},
eventHandlers: {
mousedown(e) {
const target = e.target as HTMLElement;
if (target.classList.contains('template-tag')) console.log('CLICKED TEMPLATE TAG');
// return toggleBoolean(view, view.posAtDOM(target));
},
},
},
);
}
function isSelectionInsideNode(view: EditorView, node: SyntaxNodeRef) {
for (const r of view.state.selection.ranges) {
if (r.from > node.from && r.to < node.to) return true;
}
return false;
}

View File

@@ -1,7 +1,8 @@
// This file was generated by lezer-generator. You probably shouldn't edit it.
export const Template = 1,
export const
Template = 1,
Tag = 2,
TagOpen = 3,
TagContent = 4,
Open = 3,
Close = 5,
Text = 6;
TagClose = 5,
Text = 6

View File

@@ -16,4 +16,3 @@ export const parser = LRParser.deserialize({
topRules: {"Template":[0,1]},
tokenPrec: 0
})

View File

@@ -2,6 +2,8 @@ import { styleTags, tags as t } from '@lezer/highlight';
export const highlight = styleTags({
Protocol: t.comment,
Placeholder: t.emphasis,
// PathSegment: t.tagName,
// Port: t.attributeName,
// Host: t.variableName,
// Path: t.bool,

View File

@@ -1,18 +1,20 @@
@top url { Protocol? Host Port? Path? Query? }
Query {
"?" queryPair ("&" queryPair)*
}
Path { ("/" (Placeholder | PathSegment))+ }
Query { "?" queryPair ("&" queryPair)* }
@tokens {
Protocol { $[a-zA-Z]+ "://" }
Path { ("/" $[a-zA-Z0-9\-_.]*)+ }
queryPair { ($[a-zA-Z0-9]+ ("=" $[a-zA-Z0-9]*)?) }
Port { ":" $[0-9]+ }
Host { $[a-zA-Z0-9-_.]+ }
Port { ":" $[0-9]+ }
Placeholder { ":" ![/?#]+ }
PathSegment { ![?#/]+ }
queryPair { ($[a-zA-Z0-9]+ ("=" $[a-zA-Z0-9]*)?) }
// Protocol/host overlaps, so give proto explicit precedence
@precedence { Protocol, Host }
@precedence { Placeholder, PathSegment }
}
@external propSource highlight from "./highlight"

View File

@@ -5,4 +5,6 @@ export const
Host = 3,
Port = 4,
Path = 5,
Query = 6
Placeholder = 6,
PathSegment = 7,
Query = 8

View File

@@ -3,16 +3,17 @@ import {LRParser} from "@lezer/lr"
import {highlight} from "./highlight"
export const parser = LRParser.deserialize({
version: 14,
states: "!jOQOPOOQYOPOOOTOPOOOeOQO'#CbQOOOOOQ`OPOOQ]OPOOOjOPO,58|OrOQO'#CcOwOPO1G.hOOOO,58},58}OOOO-E6a-E6a",
stateData: "!S~OQQORPO~OSUOTTOXRO~OYVO~OZWOWUa~OYYO~OZWOWUi~OQR~",
goto: "dWPPPPPPX^VSPTUQXVRZX",
nodeNames: "⚠ url Protocol Host Port Path Query",
maxTerm: 11,
states: "#SOQOPOOQYOPOOOTOPOOOeOQO'#CeOmOPO'#CaOxOSO'#CdQOOOOOQ`OPOOQ]OPOOOOOO,59P,59POOOO-E6c-E6cO}OPO,59OO!VOSO'#CfO![OPO1G.jOOOO,59Q,59QOOOO-E6d-E6d",
stateData: "!j~OQQORPO~OSWO[RO]TO~OUXOVXO~O[ROZTX]TX~O^ZO~O_[OZWa~O^^O~O_[OZWi~OQRUVU~",
goto: "rZPPPPP[PP`elTVPWVUPVWSSPWRYSQ]ZR_]",
nodeNames: "⚠ url Protocol Host Port Path Placeholder PathSegment Query",
maxTerm: 15,
propSources: [highlight],
skippedNodes: [0],
repeatNodeCount: 1,
tokenData: "%[~RYvwq}!Ov!O!Pv!P!Q!_!Q![!y![!]#u!a!b$T!c!}$Y#R#Sv#T#o$Y~vOZ~P{URP}!Ov!O!Pv!Q![v!c!}v#R#Sv#T#ov~!dVT~}!O!_!O!P!_!P!Q!_!Q![!_!c!}!_#R#S!_#T#o!_R#QVYQRP}!Ov!O!Pv!Q![!y!_!`#g!c!}!y#R#Sv#T#o!yQ#lRYQ!Q![#g!c!}#g#T#o#g~#xP!Q![#{~$QPS~!Q![#{~$YOX~R$aWYQRP}!Ov!O!Pv!Q![!y![!]$y!_!`#g!c!}$Y#R#Sv#T#o$YP$|P!P!Q%PP%SP!P!Q%VP%[OQP",
tokenizers: [0, 1],
repeatNodeCount: 2,
tokenData: "+U~RdOs!atv!avw#Ow}!a}!O#i!O!P#i!P!Q$o!Q![$t![!]&|!]!a!a!a!b(w!b!c!a!c!}(|!}#R!a#R#S#i#S#T!a#T#o(|#o;'S!a;'S;=`!x<%lO!aQ!fUVQOs!at!P!a!Q!a!a!b;'S!a;'S;=`!x<%lO!aQ!{P;=`<%l!aR#VU_PVQOs!at!P!a!Q!a!a!b;'S!a;'S;=`!x<%lO!aR#p_RPVQOs!at}!a}!O#i!O!P#i!Q![#i![!a!a!b!c!a!c!}#i!}#R!a#R#S#i#S#T!a#T#o#i#o;'S!a;'S;=`!x<%lO!a~$tO[~V$}a^SRPVQOs!at}!a}!O#i!O!P#i!Q![$t![!_!a!_!`&S!`!a!a!b!c!a!c!}$t!}#R!a#R#S#i#S#T!a#T#o$t#o;'S!a;'S;=`!x<%lO!aU&ZZ^SVQOs!at!P!a!Q![&S![!a!a!b!c!a!c!}&S!}#T!a#T#o&S#o;'S!a;'S;=`!x<%lO!aR'RVVQOs'ht!P'h!Q![(X![!a'h!b;'S'h;'S;=`(R<%lO'hQ'oUUQVQOs'ht!P'h!Q!a'h!b;'S'h;'S;=`(R<%lO'hQ(UP;=`<%l'hR(bVSPUQVQOs'ht!P'h!Q![(X![!a'h!b;'S'h;'S;=`(R<%lO'h~(|O]~V)Vb^SRPVQOs!at}!a}!O#i!O!P#i!Q![$t![!]*_!]!_!a!_!`&S!`!a!a!b!c!a!c!}(|!}#R!a#R#S#i#S#T!a#T#o(|#o;'S!a;'S;=`!x<%lO!aR*dVVQOs!at!P!a!P!Q*y!Q!a!a!b;'S!a;'S;=`!x<%lO!aP*|P!P!Q+PP+UOQP",
tokenizers: [0, 1, 2],
topRules: {"url":[0,1]},
tokenPrec: 47
tokenPrec: 66,
termNames: {"0":"⚠","1":"@top","2":"Protocol","3":"Host","4":"Port","5":"Path","6":"Placeholder","7":"PathSegment","8":"Query","9":"(\"/\" (Placeholder | PathSegment))+","10":"(\"&\" queryPair)+","11":"␄","12":"\"/\"","13":"\"?\"","14":"queryPair","15":"\"&\""}
})

View File

@@ -58,9 +58,9 @@ export const IconButton = forwardRef<HTMLButtonElement, Props>(function IconButt
'!px-0',
color === 'custom' && 'text-text-subtle',
color === 'default' && 'text-text-subtle',
size === 'md' && 'w-9',
size === 'sm' && 'w-8',
size === 'xs' && 'w-6',
size === 'md' && 'w-md',
size === 'sm' && 'w-sm',
size === 'xs' && 'w-xs',
size === '2xs' && 'w-5',
)}
{...props}

View File

@@ -6,6 +6,7 @@ export function InlineCode({ className, ...props }: HTMLAttributes<HTMLSpanEleme
<code
className={classNames(
className,
'select-text cursor-text',
'font-mono text-shrink bg-surface-highlight border border-border-subtle',
'px-1.5 py-0.5 rounded text shadow-inner break-words',
)}

View File

@@ -14,7 +14,7 @@ export type InputProps = Omit<
> &
Pick<
EditorProps,
| 'contentType'
| 'language'
| 'useTemplating'
| 'autocomplete'
| 'forceUpdateKey'
@@ -22,6 +22,7 @@ export type InputProps = Omit<
| 'autoSelect'
| 'autocompleteVariables'
| 'onKeyDown'
| 'readOnly'
> & {
name: string;
type?: 'text' | 'password';
@@ -68,6 +69,7 @@ export const Input = forwardRef<EditorView | undefined, InputProps>(function Inp
size = 'md',
type = 'text',
validate,
readOnly,
...props
}: InputProps,
ref,
@@ -77,9 +79,10 @@ export const Input = forwardRef<EditorView | undefined, InputProps>(function Inp
const [focused, setFocused] = useState(false);
const handleFocus = useCallback(() => {
if (readOnly) return;
setFocused(true);
onFocus?.();
}, [onFocus]);
}, [onFocus, readOnly]);
const handleBlur = useCallback(() => {
setFocused(false);
@@ -179,6 +182,7 @@ export const Input = forwardRef<EditorView | undefined, InputProps>(function Inp
className={editorClassName}
onFocus={handleFocus}
onBlur={handleBlur}
readOnly={readOnly}
{...props}
/>
</HStack>

View File

@@ -1,6 +1,15 @@
import classNames from 'classnames';
import type { EditorView } from 'codemirror';
import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
Fragment,
forwardRef,
useCallback,
useEffect,
useImperativeHandle,
useMemo,
useRef,
useState,
} from 'react';
import type { XYCoord } from 'react-dnd';
import { useDrag, useDrop } from 'react-dnd';
import { v4 as uuid } from 'uuid';
@@ -16,6 +25,10 @@ import type { InputProps } from './Input';
import { Input } from './Input';
import { RadioDropdown } from './RadioDropdown';
export interface PairEditorRef {
focusValue(index: number): void;
}
export type PairEditorProps = {
pairs: Pair[];
onChange: (pairs: Pair[]) => void;
@@ -41,6 +54,7 @@ export type Pair = {
value: string;
contentType?: string;
isFile?: boolean;
readOnlyName?: boolean;
};
type PairContainer = {
@@ -48,24 +62,28 @@ type PairContainer = {
id: string;
};
export function PairEditor({
className,
forceUpdateKey,
nameAutocomplete,
nameAutocompleteVariables,
namePlaceholder,
nameValidate,
valueType,
onChange,
noScroll,
pairs: originalPairs,
valueAutocomplete,
valueAutocompleteVariables,
valuePlaceholder,
valueValidate,
allowFileValues,
}: PairEditorProps) {
const [forceFocusPairId, setForceFocusPairId] = useState<string | null>(null);
export const PairEditor = forwardRef<PairEditorRef, PairEditorProps>(function PairEditor(
{
className,
forceUpdateKey,
nameAutocomplete,
nameAutocompleteVariables,
namePlaceholder,
nameValidate,
valueType,
onChange,
noScroll,
pairs: originalPairs,
valueAutocomplete,
valueAutocompleteVariables,
valuePlaceholder,
valueValidate,
allowFileValues,
}: PairEditorProps,
ref,
) {
const [forceFocusNamePairId, setForceFocusNamePairId] = useState<string | null>(null);
const [forceFocusValuePairId, setForceFocusValuePairId] = useState<string | null>(null);
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
const [pairs, setPairs] = useState<PairContainer[]>(() => {
// Remove empty headers on initial render
@@ -74,6 +92,13 @@ export function PairEditor({
return [...pairs, newPairContainer()];
});
useImperativeHandle(ref, () => ({
focusValue(index: number) {
const id = pairs[index]?.id ?? 'n/a';
setForceFocusValuePairId(id);
},
}));
useEffect(() => {
// Remove empty headers on initial render
// TODO: Make this not refresh the entire editor when forceUpdateKey changes, using some
@@ -134,17 +159,18 @@ export function PairEditor({
if (focusPrevious) {
const index = pairs.findIndex((p) => p.id === pair.id);
const id = pairs[index - 1]?.id ?? null;
setForceFocusPairId(id);
setForceFocusNamePairId(id);
}
return setPairsAndSave((oldPairs) => oldPairs.filter((p) => p.id !== pair.id));
},
[setPairsAndSave, setForceFocusPairId, pairs],
[setPairsAndSave, setForceFocusNamePairId, pairs],
);
const handleFocus = useCallback(
(pair: PairContainer) =>
setPairs((pairs) => {
setForceFocusPairId(null); // Remove focus override when something focused
setForceFocusNamePairId(null); // Remove focus override when something focused
setForceFocusValuePairId(null); // Remove focus override when something focused
const isLast = pair.id === pairs[pairs.length - 1]?.id;
return isLast ? [...pairs, newPairContainer()] : pairs;
}),
@@ -184,7 +210,8 @@ export function PairEditor({
nameAutocompleteVariables={nameAutocompleteVariables}
valueAutocompleteVariables={valueAutocompleteVariables}
valueType={valueType}
forceFocusPairId={forceFocusPairId}
forceFocusNamePairId={forceFocusNamePairId}
forceFocusValuePairId={forceFocusValuePairId}
forceUpdateKey={forceUpdateKey}
nameAutocomplete={nameAutocomplete}
valueAutocomplete={valueAutocomplete}
@@ -203,7 +230,7 @@ export function PairEditor({
})}
</div>
);
}
});
enum ItemTypes {
ROW = 'pair-row',
@@ -212,7 +239,8 @@ enum ItemTypes {
type PairEditorRowProps = {
className?: string;
pairContainer: PairContainer;
forceFocusPairId?: string | null;
forceFocusNamePairId?: string | null;
forceFocusValuePairId?: string | null;
onMove: (id: string, side: 'above' | 'below') => void;
onEnd: (id: string) => void;
onChange: (pair: PairContainer) => void;
@@ -238,7 +266,8 @@ type PairEditorRowProps = {
function PairEditorRow({
allowFileValues,
className,
forceFocusPairId,
forceFocusNamePairId,
forceFocusValuePairId,
forceUpdateKey,
isLast,
nameAutocomplete,
@@ -254,19 +283,26 @@ function PairEditorRow({
valueAutocomplete,
valueAutocompleteVariables,
valuePlaceholder,
valueValidate,
valueType,
valueValidate,
}: PairEditorRowProps) {
const { id } = pairContainer;
const ref = useRef<HTMLDivElement>(null);
const prompt = usePrompt();
const nameInputRef = useRef<EditorView>(null);
const valueInputRef = useRef<EditorView>(null);
useEffect(() => {
if (forceFocusPairId === pairContainer.id) {
if (forceFocusNamePairId === pairContainer.id) {
nameInputRef.current?.focus();
}
}, [forceFocusPairId, pairContainer.id]);
}, [forceFocusNamePairId, pairContainer.id]);
useEffect(() => {
if (forceFocusValuePairId === pairContainer.id) {
valueInputRef.current?.focus();
}
}, [forceFocusValuePairId, pairContainer.id]);
const handleChangeEnabled = useMemo(
() => (enabled: boolean) => onChange({ id, pair: { ...pairContainer.pair, enabled } }),
@@ -374,6 +410,7 @@ function PairEditorRow({
ref={nameInputRef}
hideLabel
useTemplating
readOnly={pairContainer.pair.readOnlyName}
size="sm"
require={!isLast && !!pairContainer.pair.enabled && !!pairContainer.pair.value}
validate={nameValidate}
@@ -398,6 +435,7 @@ function PairEditorRow({
/>
) : (
<Input
ref={valueInputRef}
hideLabel
useTemplating
size="sm"
@@ -476,7 +514,14 @@ function PairEditorRow({
</RadioDropdown>
) : (
<Dropdown
items={[{ key: 'delete', label: 'Delete', onSelect: handleDelete, variant: 'danger' }]}
items={[
{
key: 'delete',
label: 'Delete',
onSelect: handleDelete,
variant: 'danger',
},
]}
>
<IconButton
iconSize="sm"

View File

@@ -1,15 +1,19 @@
import classNames from 'classnames';
import { forwardRef } from 'react';
import { useKeyValue } from '../../hooks/useKeyValue';
import { BulkPairEditor } from './BulkPairEditor';
import { IconButton } from './IconButton';
import type { PairEditorProps } from './PairEditor';
import type { PairEditorProps, PairEditorRef } from './PairEditor';
import { PairEditor } from './PairEditor';
interface Props extends PairEditorProps {
preferenceName: string;
}
export function PairOrBulkEditor({ preferenceName, ...props }: Props) {
export const PairOrBulkEditor = forwardRef<PairEditorRef, Props>(function PairOrBulkEditor(
{ preferenceName, ...props }: Props,
ref,
) {
const { value: useBulk, set: setUseBulk } = useKeyValue<boolean>({
namespace: 'global',
key: ['bulk_edit', preferenceName],
@@ -18,7 +22,7 @@ export function PairOrBulkEditor({ preferenceName, ...props }: Props) {
return (
<div className="relative h-full w-full group/wrapper">
{useBulk ? <BulkPairEditor {...props} /> : <PairEditor {...props} />}
{useBulk ? <BulkPairEditor {...props} /> : <PairEditor ref={ref} {...props} />}
<div className="absolute right-0 bottom-0">
<IconButton
size="sm"
@@ -34,4 +38,4 @@ export function PairOrBulkEditor({ preferenceName, ...props }: Props) {
</div>
</div>
);
}
});

View File

@@ -1,6 +1,6 @@
import classNames from 'classnames';
import type { ReactNode } from 'react';
import { memo, useCallback, useEffect, useRef } from 'react';
import { memo, useEffect, useRef } from 'react';
import { Icon } from '../Icon';
import type { RadioDropdownProps } from '../RadioDropdown';
import { RadioDropdown } from '../RadioDropdown';
@@ -39,31 +39,23 @@ export function Tabs({
}: Props) {
const ref = useRef<HTMLDivElement | null>(null);
const handleTabChange = useCallback(
(value: string) => {
const tabs = ref.current?.querySelectorAll<HTMLDivElement>(`[data-tab]`);
for (const tab of tabs ?? []) {
const v = tab.getAttribute('data-tab');
if (v === value) {
tab.setAttribute('tabindex', '-1');
tab.setAttribute('data-state', 'active');
tab.setAttribute('aria-hidden', 'false');
tab.style.display = 'block';
} else {
tab.setAttribute('data-state', 'inactive');
tab.setAttribute('aria-hidden', 'true');
tab.style.display = 'none';
}
}
onChangeValue(value);
},
[onChangeValue],
);
// Update tabs when value changes
useEffect(() => {
if (value === undefined) return;
handleTabChange(value);
}, [handleTabChange, value]);
const tabs = ref.current?.querySelectorAll<HTMLDivElement>(`[data-tab]`);
for (const tab of tabs ?? []) {
const v = tab.getAttribute('data-tab');
if (v === value) {
tab.setAttribute('tabindex', '-1');
tab.setAttribute('data-state', 'active');
tab.setAttribute('aria-hidden', 'false');
tab.style.display = 'block';
} else {
tab.setAttribute('data-state', 'inactive');
tab.setAttribute('aria-hidden', 'true');
tab.style.display = 'none';
}
}
}, [value]);
return (
<div
@@ -103,7 +95,7 @@ export function Tabs({
onChange={t.options.onChange}
>
<button
onClick={isActive ? undefined : () => handleTabChange(t.value)}
onClick={isActive ? undefined : () => onChangeValue(t.value)}
className={btnClassName}
>
{option && 'shortLabel' in option
@@ -121,7 +113,7 @@ export function Tabs({
return (
<button
key={t.value}
onClick={() => handleTabChange(t.value)}
onClick={() => onChangeValue(t.value)}
className={btnClassName}
>
{t.label}

View File

@@ -0,0 +1,37 @@
import type { HttpResponse } from '@yaakapp/api';
import { useContentTypeFromHeaders } from '../../hooks/useContentTypeFromHeaders';
import { useResponseBodyText } from '../../hooks/useResponseBodyText';
import { isJSON, languageFromContentType } from '../../lib/contentType';
import { BinaryViewer } from './BinaryViewer';
import { TextViewer } from './TextViewer';
import { WebPageViewer } from './WebPageViewer';
interface Props {
response: HttpResponse;
pretty: boolean;
textViewerClassName?: string;
}
export function HTMLOrTextViewer({ response, pretty, textViewerClassName }: Props) {
const rawBody = useResponseBodyText(response);
let language = languageFromContentType(useContentTypeFromHeaders(response.headers));
// A lot of APIs return JSON with `text/html` content type, so interpret as JSON if so
if (language === 'html' && isJSON(rawBody.data ?? '')) {
language = 'json';
}
if (rawBody.isLoading) {
return null;
}
if (rawBody.data == null) {
return <BinaryViewer response={response} />;
}
if (language === 'html' && pretty) {
return <WebPageViewer response={response} />;
} else {
return <TextViewer response={response} pretty={pretty} className={textViewerClassName} />;
}
}

View File

@@ -1,3 +1,4 @@
import type { HttpResponse } from '@yaakapp/api';
import classNames from 'classnames';
import type { ReactNode } from 'react';
import { useCallback, useMemo } from 'react';
@@ -8,8 +9,8 @@ import { useFilterResponse } from '../../hooks/useFilterResponse';
import { useResponseBodyText } from '../../hooks/useResponseBodyText';
import { useSaveResponse } from '../../hooks/useSaveResponse';
import { useToggle } from '../../hooks/useToggle';
import { isJSON, languageFromContentType } from '../../lib/contentType';
import { tryFormatJson, tryFormatXml } from '../../lib/formatters';
import type { HttpResponse } from '@yaakapp/api';
import { CopyButton } from '../CopyButton';
import { Banner } from '../core/Banner';
import { Button } from '../core/Button';
@@ -45,9 +46,15 @@ export function TextViewer({ response, pretty, className }: Props) {
[setFilterTextMap, response],
);
const saveResponse = useSaveResponse(response);
const contentType = useContentTypeFromHeaders(response.headers);
const rawBody = useResponseBodyText(response);
const saveResponse = useSaveResponse(response);
let language = languageFromContentType(useContentTypeFromHeaders(response.headers));
// A lot of APIs return JSON with `text/html` content type, so interpret as JSON if so
if (language === 'html' && isJSON(rawBody.data ?? '')) {
language = 'json';
}
const isSearching = filterText != null;
const filteredResponse = useFilterResponse({
@@ -63,9 +70,7 @@ export function TextViewer({ response, pretty, className }: Props) {
}
}, [isSearching, setFilterText]);
const isJson = contentType?.includes('json');
const isXml = contentType?.includes('xml') || contentType?.includes('html');
const canFilter = isJson || isXml;
const canFilter = language === 'json' || language === 'xml' || language === 'html';
const actions = useMemo<ReactNode[]>(() => {
const nodes: ReactNode[] = [];
@@ -82,7 +87,7 @@ export function TextViewer({ response, pretty, className }: Props) {
autoFocus
containerClassName="bg-surface"
size="sm"
placeholder={isJson ? 'JSONPath expression' : 'XPath expression'}
placeholder={language === 'json' ? 'JSONPath expression' : 'XPath expression'}
label="Filter expression"
name="filter"
defaultValue={filterText}
@@ -109,8 +114,8 @@ export function TextViewer({ response, pretty, className }: Props) {
canFilter,
filterText,
filteredResponse.error,
isJson,
isSearching,
language,
response.id,
setFilterText,
toggleSearch,
@@ -153,9 +158,9 @@ export function TextViewer({ response, pretty, className }: Props) {
}
const formattedBody =
pretty && contentType?.includes('json')
pretty && language === 'json'
? tryFormatJson(rawBody.data)
: pretty && contentType?.includes('xml')
: pretty && (language === 'xml' || language === 'html')
? tryFormatXml(rawBody.data)
: rawBody.data;
@@ -176,7 +181,7 @@ export function TextViewer({ response, pretty, className }: Props) {
className={className}
forceUpdateKey={body}
defaultValue={body}
contentType={contentType}
language={language}
actions={actions}
extraExtensions={extraExtensions}
/>

View File

@@ -14,9 +14,11 @@ export function useActiveWorkspaceChangedToast() {
setId(activeWorkspace?.id ?? null);
// Don't notify on first load
// Don't notify on the first load
if (id === null) return;
console.log('HELLO?', activeWorkspace?.id, id, window.location);
toast.show({
id: 'workspace-changed',
timeout: 3000,

View File

@@ -1,5 +1,5 @@
import { useMemo } from 'react';
import type { HttpResponseHeader } from '@yaakapp/api';
import { useMemo } from 'react';
export function useContentTypeFromHeaders(headers: HttpResponseHeader[] | null): string | null {
return useMemo(

View File

@@ -0,0 +1,15 @@
import { useMutation } from '@tanstack/react-query';
import type { HttpResponse } from '@yaakapp/api';
import { useCopy } from './useCopy';
import { getResponseBodyText } from '../lib/responseBody';
export function useCopyHttpResponse(response: HttpResponse) {
const copy = useCopy();
return useMutation({
mutationKey: ['copy_http_response'],
async mutationFn() {
const body = await getResponseBodyText(response);
copy(body);
},
});
}

View File

@@ -13,7 +13,7 @@ export function useCreateDropdownItems({
}: {
hideFolder?: boolean;
hideIcons?: boolean;
folderId?: string;
folderId?: string | null;
} = {}): DropdownItem[] {
const createHttpRequest = useCreateHttpRequest();
const createGrpcRequest = useCreateGrpcRequest();

View File

@@ -2,13 +2,11 @@ import { useMutation } from '@tanstack/react-query';
import type { Folder } from '@yaakapp/api';
import { trackEvent } from '../lib/analytics';
import { invokeCmd } from '../lib/tauri';
import { useActiveRequest } from './useActiveRequest';
import { useActiveWorkspace } from './useActiveWorkspace';
import { usePrompt } from './usePrompt';
export function useCreateFolder() {
const workspace = useActiveWorkspace();
const activeRequest = useActiveRequest();
const prompt = usePrompt();
return useMutation<Folder, unknown, Partial<Pick<Folder, 'name' | 'sortPriority' | 'folderId'>>>({
@@ -29,7 +27,6 @@ export function useCreateFolder() {
placeholder: 'Name',
}));
patch.sortPriority = patch.sortPriority || -Date.now();
patch.folderId = patch.folderId || activeRequest?.folderId;
return invokeCmd('cmd_create_folder', { workspaceId: workspace.id, ...patch });
},
onSettled: () => trackEvent('folder', 'create'),

View File

@@ -0,0 +1,13 @@
import { useMutation } from '@tanstack/react-query';
import { trackEvent } from '../lib/analytics';
import { invokeCmd } from '../lib/tauri';
export function useCreatePlugin() {
return useMutation<void, unknown, string>({
mutationKey: ['create_plugin'],
mutationFn: async (directory: string) => {
await invokeCmd('cmd_create_plugin', { directory });
},
onSettled: () => trackEvent('plugin', 'create'),
});
}

View File

@@ -1,6 +1,6 @@
import type { HttpRequest } from '@yaakapp/api';
import type { IntrospectionQuery } from 'graphql';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { buildClientSchema, getIntrospectionQuery } from '../components/core/Editor';
import { minPromiseMillis } from '../lib/minPromiseMillis';
import { getResponseBodyText } from '../lib/responseBody';
@@ -29,8 +29,6 @@ export function useIntrospectGraphQL(baseRequest: HttpRequest) {
namespace: 'global',
});
const introspectionInterval = useRef<NodeJS.Timeout>();
useEffect(() => {
const fetchIntrospection = async () => {
setIsLoading(true);
@@ -63,16 +61,9 @@ export function useIntrospectGraphQL(baseRequest: HttpRequest) {
await setIntrospection(data);
};
const runIntrospection = () => {
fetchIntrospection()
.catch((e) => setError(e.message))
.finally(() => setIsLoading(false));
};
// Do it again on an interval
clearInterval(introspectionInterval.current);
introspectionInterval.current = setInterval(runIntrospection, 1000 * 60);
runIntrospection(); // Run immediately
fetchIntrospection()
.catch((e) => setError(e.message))
.finally(() => setIsLoading(false));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [request.id, request.url, request.method, refetchKey, activeEnvironment?.id]);

View File

@@ -0,0 +1,13 @@
import { useQuery } from '@tanstack/react-query';
import type { PluginBootResponse } from '@yaakapp/api';
import { invokeCmd } from '../lib/tauri';
export function usePluginInfo(id: string) {
return useQuery({
queryKey: ['plugin_info', id],
queryFn: async () => {
const info = (await invokeCmd('cmd_plugin_info', { id })) as PluginBootResponse;
return info;
},
});
}

View File

@@ -0,0 +1,23 @@
import { useMutation } from '@tanstack/react-query';
import type { Plugin } from '@yaakapp/api';
import { atom, useAtomValue, useSetAtom } from 'jotai';
import { minPromiseMillis } from '../lib/minPromiseMillis';
import { listPlugins } from '../lib/store';
const plugins = await listPlugins();
export const pluginsAtom = atom<Plugin[]>(plugins);
export function usePlugins() {
return useAtomValue(pluginsAtom);
}
export function useRefreshPlugins() {
const setPlugins = useSetAtom(pluginsAtom);
return useMutation({
mutationKey: ['refresh_plugins'],
mutationFn: async () => {
const plugins = await minPromiseMillis(listPlugins());
setPlugins(plugins);
},
});
}

View File

@@ -0,0 +1,65 @@
import EventEmitter from 'eventemitter3';
import type { DependencyList } from 'react';
import { useCallback, useEffect } from 'react';
type EventDataMap = {
'request_params.focus_value': string;
'request_pane.focus_tab': undefined;
};
export function useRequestEditorEvent<
Event extends keyof EventDataMap,
Data extends EventDataMap[Event],
>(event: Event, fn: (data: Data) => void, deps?: DependencyList) {
useEffect(() => {
emitter.on(event, fn);
return () => {
emitter.off(event, fn);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, deps);
}
export function useRequestEditor() {
const focusParamsTab = useCallback(() => {
emitter.emit('request_pane.focus_tab', undefined);
}, []);
const focusParamValue = useCallback(
(name: string) => {
focusParamsTab();
setTimeout(() => emitter.emit('request_params.focus_value', name), 50);
},
[focusParamsTab],
);
return {
focusParamValue,
focusParamsTab,
};
}
const emitter = new (class RequestEditorEventEmitter {
#emitter: EventEmitter = new EventEmitter();
emit<Event extends keyof EventDataMap, Data extends EventDataMap[Event]>(
event: Event,
data: Data,
) {
this.#emitter.emit(event, data);
}
on<Event extends keyof EventDataMap, Data extends EventDataMap[Event]>(
event: Event,
fn: (data: Data) => void,
) {
this.#emitter.on(event, fn);
}
off<Event extends keyof EventDataMap, Data extends EventDataMap[Event]>(
event: Event,
fn: (data: Data) => void,
) {
this.#emitter.off(event, fn);
}
})();

View File

@@ -4,7 +4,7 @@ import { getResponseBodyText } from '../lib/responseBody';
export function useResponseBodyText(response: HttpResponse) {
return useQuery<string | null>({
queryKey: ['response-body-text', response?.updatedAt],
queryKey: ['response-body-text', response.id, response?.updatedAt],
queryFn: () => getResponseBodyText(response),
});
}

View File

@@ -13,6 +13,7 @@ export type TrackResource =
| 'http_request'
| 'http_response'
| 'key_value'
| 'plugin'
| 'setting'
| 'sidebar'
| 'theme'

View File

@@ -0,0 +1,25 @@
import type { EditorProps } from '../components/core/Editor';
export function languageFromContentType(contentType: string | null): EditorProps['language'] {
const justContentType = contentType?.split(';')[0] ?? contentType ?? '';
if (justContentType.includes('json')) {
return 'json';
} else if (justContentType.includes('xml')) {
return 'xml';
} else if (justContentType.includes('html')) {
return 'html';
} else if (justContentType.includes('javascript')) {
return 'javascript';
} else {
return 'text';
}
}
export function isJSON(text: string): boolean {
try {
JSON.parse(text);
return true;
} catch (_) {
return false;
}
}

View File

@@ -1,7 +1,7 @@
import { sleep } from './sleep';
/** Ensures a promise takes at least a certain number of milliseconds to resolve */
export async function minPromiseMillis<T>(promise: Promise<T>, millis: number) {
export async function minPromiseMillis<T>(promise: Promise<T>, millis = 300) {
const start = Date.now();
let result;
let err;

View File

@@ -46,3 +46,8 @@ export function modelsEq(a: Model, b: Model) {
export function getContentTypeHeader(headers: HttpResponseHeader[]): string | null {
return headers.find((h) => h.name.toLowerCase() === 'content-type')?.value ?? null;
}
export function getCharsetFromContentType(headers: HttpResponseHeader[]): string | null {
const contentType = getContentTypeHeader(headers);
return contentType?.match(/charset=([^ ;]+)/)?.[1] ?? null;
}

View File

@@ -1,9 +1,18 @@
import { readFile, readTextFile } from '@tauri-apps/plugin-fs';
import { readFile } from '@tauri-apps/plugin-fs';
import type { HttpResponse } from '@yaakapp/api';
import { getCharsetFromContentType } from './models';
export async function getResponseBodyText(response: HttpResponse): Promise<string | null> {
if (response.bodyPath) {
return await readTextFile(response.bodyPath);
const bytes = await readFile(response.bodyPath);
const charset = getCharsetFromContentType(response.headers);
try {
return new TextDecoder(charset ?? 'utf-8', { fatal: true }).decode(bytes);
} catch (_) {
// Failed to decode as text, so return null
return null;
}
}
return null;
}

View File

@@ -4,6 +4,7 @@ import type {
Folder,
GrpcRequest,
HttpRequest,
Plugin,
Settings,
Workspace,
} from '@yaakapp/api';
@@ -63,6 +64,11 @@ export async function listWorkspaces(): Promise<Workspace[]> {
return workspaces;
}
export async function listPlugins(): Promise<Plugin[]> {
const plugins: Plugin[] = (await invokeCmd('cmd_list_plugins')) ?? [];
return plugins;
}
export async function getCookieJar(id: string | null): Promise<CookieJar | null> {
if (id === null) return null;
const cookieJar: CookieJar = (await invokeCmd('cmd_get_cookie_jar', { id })) ?? null;

View File

@@ -6,6 +6,7 @@ type TauriCmd =
| 'cmd_check_for_updates'
| 'cmd_create_cookie_jar'
| 'cmd_create_environment'
| 'cmd_create_plugin'
| 'cmd_template_tokens_to_string'
| 'cmd_create_folder'
| 'cmd_create_grpc_request'
@@ -47,11 +48,13 @@ type TauriCmd =
| 'cmd_list_grpc_requests'
| 'cmd_list_http_requests'
| 'cmd_list_http_responses'
| 'cmd_list_plugins'
| 'cmd_list_workspaces'
| 'cmd_metadata'
| 'cmd_new_nested_window'
| 'cmd_new_window'
| 'cmd_parse_template'
| 'cmd_plugin_info'
| 'cmd_render_template'
| 'cmd_save_response'
| 'cmd_send_ephemeral_request'

View File

@@ -1,5 +1,4 @@
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
import { attachConsole } from '@tauri-apps/plugin-log';
import { type } from '@tauri-apps/plugin-os';
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
@@ -18,8 +17,6 @@ if (osType !== 'macos') {
await getCurrentWebviewWindow().setDecorations(false);
}
await attachConsole();
window.addEventListener('keydown', (e) => {
// Hack to not go back in history on backspace. Check for document body
// or else it will prevent backspace in input fields.

View File

@@ -1,6 +1,7 @@
const plugin = require('tailwindcss/plugin');
const height = {
const sizes = {
'2xs': '1.25rem',
xs: '1.8rem',
sm: '2.0rem',
md: '2.5rem',
@@ -18,8 +19,10 @@ module.exports = {
fontSize: {
xs: '0.8rem',
},
height,
minHeight: height,
height: sizes,
width: sizes,
minHeight: sizes,
minWidth: sizes,
lineHeight: {
// HACK: Minus 2 to account for borders inside inputs
xs: 'calc(1.75rem - 2px)',