mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-02-16 12:47:46 +01:00
Compare commits
47 Commits
v2023.3.0
...
v2024.0.1-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3539642491 | ||
|
|
08abea6a6f | ||
|
|
0045b85f00 | ||
|
|
4b34c3d101 | ||
|
|
4af0a15d9f | ||
|
|
3a4a76c58d | ||
|
|
3086d815c1 | ||
|
|
a48a9eab4a | ||
|
|
48664c66e5 | ||
|
|
7aee5176a9 | ||
|
|
0da68ced18 | ||
|
|
39f7d9c113 | ||
|
|
138943bfb6 | ||
|
|
c1c9f882a6 | ||
|
|
1bcf26f656 | ||
|
|
7c2466da5e | ||
|
|
7dc78a1f6f | ||
|
|
88d024023b | ||
|
|
626aacf982 | ||
|
|
d5855c45a6 | ||
|
|
793bff9f27 | ||
|
|
88ea68e72f | ||
|
|
35e40d2c55 | ||
|
|
c472b83409 | ||
|
|
52c26d235c | ||
|
|
ac54729012 | ||
|
|
0586034ef4 | ||
|
|
91790ba708 | ||
|
|
d8ab6c0b50 | ||
|
|
b600a21a2b | ||
|
|
4f9d1278f7 | ||
|
|
15aa93f5f9 | ||
|
|
c7798092d8 | ||
|
|
5560593aaa | ||
|
|
66639e651d | ||
|
|
8e42d5ccdb | ||
|
|
5c62594087 | ||
|
|
26b6c48657 | ||
|
|
0290aba982 | ||
|
|
0bafc4e4f5 | ||
|
|
9a36f94279 | ||
|
|
1d8e66179e | ||
|
|
fda6d16d8e | ||
|
|
c4737916df | ||
|
|
919465cdbb | ||
|
|
de3730fa4f | ||
|
|
aff26fdd46 |
5
.github/workflows/artifacts.yml
vendored
5
.github/workflows/artifacts.yml
vendored
@@ -17,9 +17,8 @@ jobs:
|
||||
target: x86_64-apple-darwin
|
||||
- os: windows-2022
|
||||
target: x86_64-pc-windows-msvc
|
||||
# # Re-enable Linux when context menu is supported
|
||||
# - os: ubuntu-20.04
|
||||
# target: x86_64-unknown-linux-gnu
|
||||
- os: ubuntu-20.04
|
||||
target: x86_64-unknown-linux-gnu
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
node_modules/
|
||||
dist/
|
||||
out/
|
||||
.prettierrc.cjs
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
<script value="start" />
|
||||
</scripts>
|
||||
<node-interpreter value="project" />
|
||||
<envs />
|
||||
<envs>
|
||||
</envs>
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
</component>
|
||||
|
||||
11
index.html
11
index.html
@@ -5,17 +5,6 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Yaak App</title>
|
||||
<!-- <script src="http://localhost:8097"></script>-->
|
||||
<style>
|
||||
body {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background-color: black;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
1678
package-lock.json
generated
1678
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
17
package.json
17
package.json
@@ -5,18 +5,18 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "npm run build:plugins && npm run tauri-dev",
|
||||
"tauri-dev": "YAAK_ENV=development tauri dev --no-watch --config src-tauri/tauri-dev.conf.json",
|
||||
"tauri-dev": "tauri dev --no-watch --config ./src-tauri/tauri-dev.conf.json",
|
||||
"tauri-build": "tauri build",
|
||||
"tauri": "tauri",
|
||||
"build": "npm run build:frontend",
|
||||
"dev": "vite dev",
|
||||
"lint": "tsc && eslint . --ext .ts,.tsx",
|
||||
"build:icon:release": "tauri icon design/icon.png --output src-tauri/icons/release",
|
||||
"build:icon:dev": "tauri icon design/icon-dev.png --output src-tauri/icons/dev",
|
||||
"build:icon:release": "tauri icon design/icon.png --output ./src-tauri/icons/release",
|
||||
"build:icon:dev": "tauri icon design/icon-dev.png --output ./src-tauri/icons/dev",
|
||||
"build:frontend": "vite build",
|
||||
"build:plugins": "run-p build:plugin:importer-insomnia build:plugin:importer-postman build:plugin:importer-yaak",
|
||||
"build:plugin:importer-insomnia": "cd src-tauri/plugins/importer-insomnia && vite build",
|
||||
"build:plugin:importer-postman": "cd src-tauri/plugins/importer-postman && vite build",
|
||||
"build:plugin:importer-insomnia": "cd ./src-tauri/plugins/importer-insomnia && vite build",
|
||||
"build:plugin:importer-postman": "cd ./src-tauri/plugins/importer-postman && vite build",
|
||||
"build:plugin:importer-yaak": "cd src-tauri/plugins/importer-yaak && vite build",
|
||||
"test": "vitest",
|
||||
"coverage": "vitest run --coverage",
|
||||
@@ -39,7 +39,7 @@
|
||||
"@tanstack/react-query": "^4.28.0",
|
||||
"@tanstack/react-query-devtools": "^4.28.0",
|
||||
"@tanstack/react-query-persist-client": "^4.28.0",
|
||||
"@tauri-apps/api": "^1.5.1",
|
||||
"@tauri-apps/api": "^1.5.3",
|
||||
"buffer": "^6.0.3",
|
||||
"classnames": "^2.3.2",
|
||||
"cm6-graphql": "^0.0.9",
|
||||
@@ -56,13 +56,13 @@
|
||||
"react-helmet-async": "^1.3.0",
|
||||
"react-router-dom": "^6.8.1",
|
||||
"react-use": "^17.4.0",
|
||||
"tauri-plugin-context-menu": "^0.5.0",
|
||||
"slugify": "^1.6.6",
|
||||
"tauri-plugin-log-api": "github:tauri-apps/tauri-plugin-log#v1",
|
||||
"uuid": "^9.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/nesting": "^0.0.0-insiders.565cd3e",
|
||||
"@tauri-apps/cli": "^1.5.4",
|
||||
"@tauri-apps/cli": "^1.5.6",
|
||||
"@types/node": "^18.7.10",
|
||||
"@types/papaparse": "^5.3.7",
|
||||
"@types/parse-color": "^1.0.1",
|
||||
@@ -86,6 +86,7 @@
|
||||
"postcss": "^8.4.21",
|
||||
"postcss-nesting": "^11.2.1",
|
||||
"prettier": "^2.8.4",
|
||||
"react-devtools": "^4.28.5",
|
||||
"tailwindcss": "^3.2.7",
|
||||
"typescript": "^5.0.2",
|
||||
"vite": "^4.0.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT id, model, workspace_id, request_id, updated_at, created_at, url,\n status, status_reason, content_length, body, body_path, elapsed, error,\n headers AS \"headers!: sqlx::types::Json<Vec<HttpResponseHeader>>\"\n FROM http_responses\n WHERE workspace_id = ?\n ORDER BY created_at DESC\n ",
|
||||
"query": "\n SELECT id, model, workspace_id, request_id, updated_at, created_at, url,\n status, status_reason, content_length, body_path, elapsed, error,\n headers AS \"headers!: sqlx::types::Json<Vec<HttpResponseHeader>>\"\n FROM http_responses\n WHERE request_id = ?\n ORDER BY created_at DESC\n LIMIT ?\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -53,34 +53,29 @@
|
||||
"ordinal": 9,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "body",
|
||||
"ordinal": 10,
|
||||
"type_info": "Blob"
|
||||
},
|
||||
{
|
||||
"name": "body_path",
|
||||
"ordinal": 11,
|
||||
"ordinal": 10,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "elapsed",
|
||||
"ordinal": 12,
|
||||
"ordinal": 11,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "error",
|
||||
"ordinal": 13,
|
||||
"ordinal": 12,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "headers!: sqlx::types::Json<Vec<HttpResponseHeader>>",
|
||||
"ordinal": 14,
|
||||
"ordinal": 13,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
@@ -94,11 +89,10 @@
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "26072725d536c3cfdffd9a681d17c0ee2f246ca98e0459630a2430236d3bbdd2"
|
||||
"hash": "07b0c398efd1d5f8f479652de658716a9e7faef6aba6583dd209a4f290c5edd1"
|
||||
}
|
||||
12
src-tauri/.sqlx/query-09a8074f7ef8d734607f95391819e38f822488933905dcc7a7788bd184bc7796.json
generated
Normal file
12
src-tauri/.sqlx/query-09a8074f7ef8d734607f95391819e38f822488933905dcc7a7788bd184bc7796.json
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n UPDATE settings SET (\n follow_redirects,\n validate_certificates,\n theme,\n appearance\n ) = (?, ?, ?, ?) WHERE id = 'default';\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 4
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "09a8074f7ef8d734607f95391819e38f822488933905dcc7a7788bd184bc7796"
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n INSERT INTO http_responses (\n id,\n request_id,\n workspace_id,\n elapsed,\n url,\n status,\n status_reason,\n content_length,\n body,\n body_path,\n headers\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);\n ",
|
||||
"query": "\n INSERT INTO http_responses (\n id,\n request_id,\n workspace_id,\n elapsed,\n url,\n status,\n status_reason,\n content_length,\n body_path,\n headers\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 11
|
||||
"Right": 10
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "8947a2a90478277c42fe9b06bc1fa98197642a4d281a3dbc101be2c9c1fec36c"
|
||||
"hash": "198bd086ccc87d2e6c24cb1c717f486d3ab58c0c958ede850c018fc266eade87"
|
||||
}
|
||||
12
src-tauri/.sqlx/query-294cbe19f9ddd9519ace3558df4308948082ec0ce7096855aa7d8fba519b8b4f.json
generated
Normal file
12
src-tauri/.sqlx/query-294cbe19f9ddd9519ace3558df4308948082ec0ce7096855aa7d8fba519b8b4f.json
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n UPDATE http_responses SET (\n elapsed,\n url,\n status,\n status_reason,\n content_length,\n body_path,\n error,\n headers,\n updated_at\n ) = (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) WHERE id = ?;\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 9
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "294cbe19f9ddd9519ace3558df4308948082ec0ce7096855aa7d8fba519b8b4f"
|
||||
}
|
||||
12
src-tauri/.sqlx/query-2c181a4dc13efc52fe6a5a68291c5678a9624020df4ea744e78396f6926d5c88.json
generated
Normal file
12
src-tauri/.sqlx/query-2c181a4dc13efc52fe6a5a68291c5678a9624020df4ea744e78396f6926d5c88.json
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n INSERT INTO settings (id)\n VALUES ('default')\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 0
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "2c181a4dc13efc52fe6a5a68291c5678a9624020df4ea744e78396f6926d5c88"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT id, model, workspace_id, request_id, updated_at, created_at, url,\n status, status_reason, content_length, body, body_path, elapsed, error,\n headers AS \"headers!: sqlx::types::Json<Vec<HttpResponseHeader>>\"\n FROM http_responses\n WHERE id = ?\n ",
|
||||
"query": "\n SELECT id, model, workspace_id, request_id, updated_at, created_at, url,\n status, status_reason, content_length, body_path, elapsed, error,\n headers AS \"headers!: sqlx::types::Json<Vec<HttpResponseHeader>>\"\n FROM http_responses\n WHERE workspace_id = ?\n ORDER BY created_at DESC\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -53,29 +53,24 @@
|
||||
"ordinal": 9,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "body",
|
||||
"ordinal": 10,
|
||||
"type_info": "Blob"
|
||||
},
|
||||
{
|
||||
"name": "body_path",
|
||||
"ordinal": 11,
|
||||
"ordinal": 10,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "elapsed",
|
||||
"ordinal": 12,
|
||||
"ordinal": 11,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "error",
|
||||
"ordinal": 13,
|
||||
"ordinal": 12,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "headers!: sqlx::types::Json<Vec<HttpResponseHeader>>",
|
||||
"ordinal": 14,
|
||||
"ordinal": 13,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
@@ -94,11 +89,10 @@
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "c23c61b05a4c9e04ab0c1fc2c579d6f2a82a37aeed8addf9861b4985f2a5422e"
|
||||
"hash": "3d199d371be948211f4a50c869b307f5df60784293c52397d77a187633a406dd"
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n UPDATE http_responses SET (\n elapsed,\n url,\n status,\n status_reason,\n content_length,\n body,\n body_path,\n error,\n headers,\n updated_at\n ) = (?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) WHERE id = ?;\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 10
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "62475fd9483fb5eda01c937949da2ef66ac7005b4be06b87aa6210d462348aca"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT id, model, workspace_id, request_id, updated_at, created_at, url,\n status, status_reason, content_length, body, body_path, elapsed, error,\n headers AS \"headers!: sqlx::types::Json<Vec<HttpResponseHeader>>\"\n FROM http_responses\n WHERE request_id = ?\n ORDER BY created_at DESC\n ",
|
||||
"query": "\n SELECT id, model, workspace_id, request_id, updated_at, created_at, url,\n status, status_reason, content_length, body_path, elapsed, error,\n headers AS \"headers!: sqlx::types::Json<Vec<HttpResponseHeader>>\"\n FROM http_responses\n WHERE id = ?\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -53,29 +53,24 @@
|
||||
"ordinal": 9,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "body",
|
||||
"ordinal": 10,
|
||||
"type_info": "Blob"
|
||||
},
|
||||
{
|
||||
"name": "body_path",
|
||||
"ordinal": 11,
|
||||
"ordinal": 10,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "elapsed",
|
||||
"ordinal": 12,
|
||||
"ordinal": 11,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "error",
|
||||
"ordinal": 13,
|
||||
"ordinal": 12,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "headers!: sqlx::types::Json<Vec<HttpResponseHeader>>",
|
||||
"ordinal": 14,
|
||||
"ordinal": 13,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
@@ -94,11 +89,10 @@
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "5aa070e61995f8b1724efaa94c5f0cef5a4be6efda5d70354ad449d7d4b5aee4"
|
||||
"hash": "679a519475adeb50abf046114d3c0d1e48e103f2bb11ef47637d7f0b00ed241f"
|
||||
}
|
||||
68
src-tauri/.sqlx/query-f27d45f7ea2b04fc203e46a85be96a591a6495794dc042e1e2f3460c9ed65a5c.json
generated
Normal file
68
src-tauri/.sqlx/query-f27d45f7ea2b04fc203e46a85be96a591a6495794dc042e1e2f3460c9ed65a5c.json
generated
Normal file
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT\n id,\n model,\n created_at,\n updated_at,\n follow_redirects,\n validate_certificates,\n request_timeout,\n theme,\n appearance\n FROM settings\n WHERE id = 'default'\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "model",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "created_at",
|
||||
"ordinal": 2,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "updated_at",
|
||||
"ordinal": 3,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "follow_redirects",
|
||||
"ordinal": 4,
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"name": "validate_certificates",
|
||||
"ordinal": 5,
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"name": "request_timeout",
|
||||
"ordinal": 6,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "theme",
|
||||
"ordinal": 7,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "appearance",
|
||||
"ordinal": 8,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 0
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "f27d45f7ea2b04fc203e46a85be96a591a6495794dc042e1e2f3460c9ed65a5c"
|
||||
}
|
||||
319
src-tauri/Cargo.lock
generated
319
src-tauri/Cargo.lock
generated
@@ -81,6 +81,20 @@ version = "1.0.75"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6"
|
||||
|
||||
[[package]]
|
||||
name = "async-compression"
|
||||
version = "0.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc2d0cfb2a7388d34f590e76686704c494ed7aaceed62ee1ba35cbf363abc2a5"
|
||||
dependencies = [
|
||||
"brotli",
|
||||
"flate2",
|
||||
"futures-core",
|
||||
"memchr",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atk"
|
||||
version = "0.15.1"
|
||||
@@ -171,12 +185,6 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bit_field"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
@@ -717,30 +725,6 @@ dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-deque"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"crossbeam-epoch",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-epoch"
|
||||
version = "0.9.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"cfg-if",
|
||||
"crossbeam-utils",
|
||||
"memoffset",
|
||||
"scopeguard",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-queue"
|
||||
version = "0.3.8"
|
||||
@@ -760,12 +744,6 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crunchy"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.6"
|
||||
@@ -805,12 +783,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ctor"
|
||||
version = "0.1.26"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096"
|
||||
checksum = "30d2b3721e861707777e3195b0158f950ae6dc4a27e4d02ff9f67e3eb3de199e"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
"syn 2.0.39",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -997,11 +975,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "embed-resource"
|
||||
version = "2.4.0"
|
||||
version = "2.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f54cc3e827ee1c3812239a9a41dede7b4d7d5d5464faa32d71bd7cba28ce2cb2"
|
||||
checksum = "3bde55e389bea6a966bd467ad1ad7da0ae14546a5bc794d16d1e55e7fca44881"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"memchr",
|
||||
"rustc_version",
|
||||
"toml 0.8.8",
|
||||
"vswhom",
|
||||
@@ -1062,22 +1041,6 @@ version = "2.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
|
||||
|
||||
[[package]]
|
||||
name = "exr"
|
||||
version = "1.71.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "832a761f35ab3e6664babfbdc6cef35a4860e816ec3916dcfd0882954e98a8a8"
|
||||
dependencies = [
|
||||
"bit_field",
|
||||
"flume",
|
||||
"half",
|
||||
"lebe",
|
||||
"miniz_oxide",
|
||||
"rayon-core",
|
||||
"smallvec",
|
||||
"zune-inflate",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fast-float"
|
||||
version = "0.2.0"
|
||||
@@ -1465,16 +1428,6 @@ dependencies = [
|
||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gif"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "80792593675e051cf94a4b111980da2ba60d4a83e43e0048c5693baab3977045"
|
||||
dependencies = [
|
||||
"color_quant",
|
||||
"weezl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gimli"
|
||||
version = "0.28.0"
|
||||
@@ -1660,15 +1613,6 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "half"
|
||||
version = "2.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02b4af3693f1b705df946e9fe5631932443781d0aabb423b62fcd4d73f6d2fd0"
|
||||
dependencies = [
|
||||
"crunchy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.12.3"
|
||||
@@ -2056,14 +2000,8 @@ dependencies = [
|
||||
"bytemuck",
|
||||
"byteorder",
|
||||
"color_quant",
|
||||
"exr",
|
||||
"gif",
|
||||
"jpeg-decoder",
|
||||
"num-rational",
|
||||
"num-traits",
|
||||
"png",
|
||||
"qoi",
|
||||
"tiff",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2090,9 +2028,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "infer"
|
||||
version = "0.12.0"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a898e4b7951673fce96614ce5751d13c40fc5674bc2d759288e46c3ab62598b3"
|
||||
checksum = "f551f8c3a39f68f986517db0d1759de85881894fdc7db798bd2a9df9cb04b7fc"
|
||||
dependencies = [
|
||||
"cfb",
|
||||
]
|
||||
@@ -2196,15 +2134,6 @@ version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
|
||||
|
||||
[[package]]
|
||||
name = "jpeg-decoder"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e"
|
||||
dependencies = [
|
||||
"rayon",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.65"
|
||||
@@ -2260,12 +2189,6 @@ dependencies = [
|
||||
"spin 0.5.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lebe"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.150"
|
||||
@@ -2434,9 +2357,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.6.4"
|
||||
version = "2.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167"
|
||||
checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149"
|
||||
|
||||
[[package]]
|
||||
name = "memoffset"
|
||||
@@ -2816,6 +2739,15 @@ version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-src"
|
||||
version = "300.1.6+3.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "439fac53e092cd7442a3660c85dde4643ab3b5bd39040912388dcdabf6b88085"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.95"
|
||||
@@ -2824,6 +2756,7 @@ checksum = "40a4130519a360279579c2053038317e40eff64d13fd3f004f9e1b72b8a6aaf9"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"openssl-src",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
@@ -2946,9 +2879,7 @@ version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259"
|
||||
dependencies = [
|
||||
"phf_macros 0.10.0",
|
||||
"phf_shared 0.10.0",
|
||||
"proc-macro-hack",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3025,20 +2956,6 @@ dependencies = [
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_macros"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "58fdf3184dd560f160dd73922bea2d5cd6e8f064bf4b13110abd81b03697b4e0"
|
||||
dependencies = [
|
||||
"phf_generator 0.10.0",
|
||||
"phf_shared 0.10.0",
|
||||
"proc-macro-hack",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_macros"
|
||||
version = "0.11.2"
|
||||
@@ -3229,15 +3146,6 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "qoi"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.31.0"
|
||||
@@ -3343,26 +3251,6 @@ version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9"
|
||||
|
||||
[[package]]
|
||||
name = "rayon"
|
||||
version = "1.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c27db03db7734835b3f53954b534c91069375ce6ccaa2e065441e07d9b6cdb1"
|
||||
dependencies = [
|
||||
"either",
|
||||
"rayon-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayon-core"
|
||||
version = "1.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ce3fb6ad83f861aac485e76e1985cd109d9a3713802152be56c3b1f0e0658ed"
|
||||
dependencies = [
|
||||
"crossbeam-deque",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.1.57"
|
||||
@@ -3458,6 +3346,7 @@ version = "0.11.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "046cd98826c46c2ac8ddecae268eb5c2e58628688a5fc7a2643704a73faba95b"
|
||||
dependencies = [
|
||||
"async-compression",
|
||||
"base64 0.21.5",
|
||||
"bytes",
|
||||
"encoding_rs",
|
||||
@@ -4522,9 +4411,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-build"
|
||||
version = "1.5.0"
|
||||
version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "defbfc551bd38ab997e5f8e458f87396d2559d05ce32095076ad6c30f7fc5f9c"
|
||||
checksum = "e9914a4715e0b75d9f387a285c7e26b5bbfeb1249ad9f842675a82481565c532"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cargo_toml",
|
||||
@@ -4579,23 +4468,6 @@ dependencies = [
|
||||
"tauri-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-context-menu"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b93737f332761822f2d1ee6f7dbe4c1f94c4d03020a349bc1f8070d75b6409e8"
|
||||
dependencies = [
|
||||
"cocoa 0.24.1",
|
||||
"dispatch",
|
||||
"image",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"objc",
|
||||
"serde",
|
||||
"tauri",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-log"
|
||||
version = "0.0.0"
|
||||
@@ -4668,9 +4540,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-utils"
|
||||
version = "1.5.0"
|
||||
version = "1.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34d55e185904a84a419308d523c2c6891d5e2dbcee740c4997eb42e75a7b0f46"
|
||||
checksum = "ece74810b1d3d44f29f732a7ae09a63183d63949bbdd59c61f8ed2a1b70150db"
|
||||
dependencies = [
|
||||
"brotli",
|
||||
"ctor",
|
||||
@@ -4683,7 +4555,7 @@ dependencies = [
|
||||
"kuchikiki",
|
||||
"log",
|
||||
"memchr",
|
||||
"phf 0.10.1",
|
||||
"phf 0.11.2",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"semver",
|
||||
@@ -4691,10 +4563,10 @@ dependencies = [
|
||||
"serde_json",
|
||||
"serde_with",
|
||||
"thiserror",
|
||||
"toml 0.5.11",
|
||||
"toml 0.7.8",
|
||||
"url",
|
||||
"walkdir",
|
||||
"windows 0.39.0",
|
||||
"windows-version",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4773,17 +4645,6 @@ dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiff"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d172b0f4d3fba17ba89811858b9d3d97f928aece846475bbda076ca46736211"
|
||||
dependencies = [
|
||||
"flate2",
|
||||
"jpeg-decoder",
|
||||
"weezl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.3.30"
|
||||
@@ -5430,12 +5291,6 @@ dependencies = [
|
||||
"windows-metadata",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "weezl"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb"
|
||||
|
||||
[[package]]
|
||||
name = "whoami"
|
||||
version = "1.4.1"
|
||||
@@ -5473,6 +5328,18 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "window-shadows"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67ff424735b1ac21293b0492b069394b0a189c8a463fb015a16dea7c2e221c08"
|
||||
dependencies = [
|
||||
"cocoa 0.25.0",
|
||||
"objc",
|
||||
"raw-window-handle",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.37.0"
|
||||
@@ -5607,12 +5474,36 @@ dependencies = [
|
||||
"windows_x86_64_msvc 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm 0.52.0",
|
||||
"windows_aarch64_msvc 0.52.0",
|
||||
"windows_i686_gnu 0.52.0",
|
||||
"windows_i686_msvc 0.52.0",
|
||||
"windows_x86_64_gnu 0.52.0",
|
||||
"windows_x86_64_gnullvm 0.52.0",
|
||||
"windows_x86_64_msvc 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-tokens"
|
||||
version = "0.39.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f838de2fe15fe6bac988e74b798f26499a8b21a9d97edec321e79b28d1d7f597"
|
||||
|
||||
[[package]]
|
||||
name = "windows-version"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75aa004c988e080ad34aff5739c39d0312f4684699d6d71fc8a198d057b8b9b4"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.42.2"
|
||||
@@ -5625,6 +5516,12 @@ version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.37.0"
|
||||
@@ -5649,6 +5546,12 @@ version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.37.0"
|
||||
@@ -5673,6 +5576,12 @@ version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.37.0"
|
||||
@@ -5697,6 +5606,12 @@ version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.37.0"
|
||||
@@ -5721,6 +5636,12 @@ version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.42.2"
|
||||
@@ -5733,6 +5654,12 @@ version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.37.0"
|
||||
@@ -5757,6 +5684,12 @@ version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.5.19"
|
||||
@@ -5880,6 +5813,7 @@ dependencies = [
|
||||
"http",
|
||||
"log",
|
||||
"objc",
|
||||
"openssl-sys",
|
||||
"rand 0.8.5",
|
||||
"reqwest",
|
||||
"serde",
|
||||
@@ -5887,11 +5821,11 @@ dependencies = [
|
||||
"sqlx",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-context-menu",
|
||||
"tauri-plugin-log",
|
||||
"tauri-plugin-window-state",
|
||||
"tokio",
|
||||
"uuid",
|
||||
"window-shadows",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5998,12 +5932,3 @@ dependencies = [
|
||||
"crc32fast",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zune-inflate"
|
||||
version = "0.2.54"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02"
|
||||
dependencies = [
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
@@ -17,6 +17,9 @@ tauri-build = { version = "1.2", features = [] }
|
||||
objc = "0.2.7"
|
||||
cocoa = "0.25.0"
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
openssl-sys = {version = "0.9", features = ["vendored"] } # For Ubuntu installation to work
|
||||
|
||||
[dependencies]
|
||||
base64 = "0.21.0"
|
||||
boa_engine = "0.17.3"
|
||||
@@ -25,30 +28,35 @@ chrono = { version = "0.4.23", features = ["serde"] }
|
||||
futures = "0.3.26"
|
||||
http = "0.2.8"
|
||||
rand = "0.8.5"
|
||||
reqwest = { version = "0.11.14", features = ["json", "multipart"] }
|
||||
reqwest = { version = "0.11.14", features = ["json", "multipart", "gzip", "brotli", "deflate"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = { version = "1.0", features = ["raw_value"] }
|
||||
sqlx = { version = "0.7.2", features = ["sqlite", "runtime-tokio-rustls", "json", "chrono", "time"] }
|
||||
tauri = { version = "1.3", features = [
|
||||
tauri = { version = "1.5.2", features = [
|
||||
"config-toml",
|
||||
"devtools",
|
||||
"dialog-open",
|
||||
"dialog-save",
|
||||
"fs-read-file",
|
||||
"os-all",
|
||||
"protocol-asset",
|
||||
"shell-open",
|
||||
"updater",
|
||||
"window-start-dragging",
|
||||
"dialog-open",
|
||||
"dialog-save",
|
||||
"window-close",
|
||||
"window-maximize",
|
||||
"window-minimize",
|
||||
"window-set-decorations",
|
||||
"window-set-title",
|
||||
"window-start-dragging",
|
||||
"window-unmaximize",
|
||||
] }
|
||||
tauri-plugin-window-state = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
|
||||
tauri-plugin-log = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1", features = ["colored"] }
|
||||
tokio = { version = "1.25.0", features = ["sync"] }
|
||||
uuid = "1.3.0"
|
||||
log = "0.4.20"
|
||||
tauri-plugin-context-menu = "0.5.0"
|
||||
datetime = "0.5.2"
|
||||
window-shadows = "0.2.2"
|
||||
|
||||
[features]
|
||||
# by default Tauri runs in production mode
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
|
||||
<!-- Re-enable for sandboxing. Currently disabled because auto-updater doesn't work with sandboxing.-->
|
||||
<!-- <key>com.apple.security.app-sandbox</key> <true/>-->
|
||||
<!-- <key>com.apple.security.files.user-selected.read-write</key> <true/>-->
|
||||
<!-- <key>com.apple.security.network.client</key> <true/>-->
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
1
src-tauri/migrations/20231122055216_remove_body.sql
Normal file
1
src-tauri/migrations/20231122055216_remove_body.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE http_responses DROP COLUMN body;
|
||||
13
src-tauri/migrations/20240111221224_settings.sql
Normal file
13
src-tauri/migrations/20240111221224_settings.sql
Normal file
@@ -0,0 +1,13 @@
|
||||
CREATE TABLE settings
|
||||
(
|
||||
id TEXT NOT NULL
|
||||
PRIMARY KEY,
|
||||
model TEXT DEFAULT 'settings' NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
follow_redirects BOOLEAN DEFAULT TRUE NOT NULL,
|
||||
validate_certificates BOOLEAN DEFAULT TRUE NOT NULL,
|
||||
request_timeout INTEGER DEFAULT 0 NOT NULL,
|
||||
theme TEXT DEFAULT 'default' NOT NULL,
|
||||
appearance TEXT DEFAULT 'system' NOT NULL
|
||||
);
|
||||
@@ -1,139 +1,124 @@
|
||||
function S(e, t) {
|
||||
return (
|
||||
console.log('IMPORTING Environment', e._id, e.name, JSON.stringify(e, null, 2)),
|
||||
{
|
||||
id: e._id,
|
||||
createdAt: new Date(e.created ?? Date.now()).toISOString().replace('Z', ''),
|
||||
updatedAt: new Date(e.updated ?? Date.now()).toISOString().replace('Z', ''),
|
||||
workspaceId: t,
|
||||
model: 'environment',
|
||||
name: e.name,
|
||||
variables: Object.entries(e.data).map(([n, a]) => ({
|
||||
enabled: !0,
|
||||
name: n,
|
||||
value: `${a}`,
|
||||
})),
|
||||
}
|
||||
);
|
||||
return console.log("IMPORTING Environment", e._id, e.name, JSON.stringify(e, null, 2)), {
|
||||
id: e._id,
|
||||
createdAt: new Date(e.created ?? Date.now()).toISOString().replace("Z", ""),
|
||||
updatedAt: new Date(e.updated ?? Date.now()).toISOString().replace("Z", ""),
|
||||
workspaceId: t,
|
||||
model: "environment",
|
||||
name: e.name,
|
||||
variables: Object.entries(e.data).map(([n, a]) => ({
|
||||
enabled: !0,
|
||||
name: n,
|
||||
value: `${a}`
|
||||
}))
|
||||
};
|
||||
}
|
||||
function I(e) {
|
||||
return m(e) && e._type === 'workspace';
|
||||
return m(e) && e._type === "workspace";
|
||||
}
|
||||
function y(e) {
|
||||
return m(e) && e._type === 'request_group';
|
||||
return m(e) && e._type === "request_group";
|
||||
}
|
||||
function g(e) {
|
||||
return m(e) && e._type === 'request';
|
||||
return m(e) && e._type === "request";
|
||||
}
|
||||
function f(e) {
|
||||
return m(e) && e._type === 'environment';
|
||||
return m(e) && e._type === "environment";
|
||||
}
|
||||
function m(e) {
|
||||
return Object.prototype.toString.call(e) === '[object Object]';
|
||||
return Object.prototype.toString.call(e) === "[object Object]";
|
||||
}
|
||||
function w(e) {
|
||||
return Object.prototype.toString.call(e) === '[object String]';
|
||||
return Object.prototype.toString.call(e) === "[object String]";
|
||||
}
|
||||
function O(e) {
|
||||
return Object.entries(e).map(([t, n]) => ({
|
||||
enabled: !0,
|
||||
name: t,
|
||||
value: `${n}`,
|
||||
value: `${n}`
|
||||
}));
|
||||
}
|
||||
function l(e) {
|
||||
return w(e) ? e.replaceAll(/{{\s*(_\.)?([^}]+)\s*}}/g, '${[$2]}') : e;
|
||||
return w(e) ? e.replaceAll(/{{\s*(_\.)?([^}]+)\s*}}/g, "${[$2]}") : e;
|
||||
}
|
||||
function h(e, t, n = 0) {
|
||||
var c, o;
|
||||
console.log('IMPORTING REQUEST', e._id, e.name, JSON.stringify(e, null, 2));
|
||||
let a = null,
|
||||
r = null;
|
||||
((c = e.body) == null ? void 0 : c.mimeType) === 'application/graphql'
|
||||
? ((a = 'graphql'), (r = l(e.body.text)))
|
||||
: ((o = e.body) == null ? void 0 : o.mimeType) === 'application/json' &&
|
||||
((a = 'application/json'), (r = l(e.body.text)));
|
||||
let i = null,
|
||||
u = {};
|
||||
return (
|
||||
e.authentication.type === 'bearer'
|
||||
? ((i = 'bearer'),
|
||||
(u = {
|
||||
token: l(e.authentication.token),
|
||||
}))
|
||||
: e.authentication.type === 'basic' &&
|
||||
((i = 'basic'),
|
||||
(u = {
|
||||
username: l(e.authentication.username),
|
||||
password: l(e.authentication.password),
|
||||
})),
|
||||
{
|
||||
id: e._id,
|
||||
createdAt: new Date(e.created ?? Date.now()).toISOString().replace('Z', ''),
|
||||
updatedAt: new Date(e.updated ?? Date.now()).toISOString().replace('Z', ''),
|
||||
workspaceId: t,
|
||||
folderId: e.parentId === t ? null : e.parentId,
|
||||
model: 'http_request',
|
||||
sortPriority: n,
|
||||
name: e.name,
|
||||
url: l(e.url),
|
||||
body: r,
|
||||
bodyType: a,
|
||||
authentication: u,
|
||||
authenticationType: i,
|
||||
method: e.method,
|
||||
headers: (e.headers ?? [])
|
||||
.map(({ name: d, value: p, disabled: s }) => ({
|
||||
enabled: !s,
|
||||
name: d,
|
||||
value: p,
|
||||
}))
|
||||
.filter(({ name: d, value: p }) => d !== '' || p !== ''),
|
||||
}
|
||||
);
|
||||
console.log("IMPORTING REQUEST", e._id, e.name, JSON.stringify(e, null, 2));
|
||||
let a = null, r = null;
|
||||
((c = e.body) == null ? void 0 : c.mimeType) === "application/graphql" ? (a = "graphql", r = l(e.body.text)) : ((o = e.body) == null ? void 0 : o.mimeType) === "application/json" && (a = "application/json", r = l(e.body.text));
|
||||
let i = null, u = {};
|
||||
return e.authentication.type === "bearer" ? (i = "bearer", u = {
|
||||
token: l(e.authentication.token)
|
||||
}) : e.authentication.type === "basic" && (i = "basic", u = {
|
||||
username: l(e.authentication.username),
|
||||
password: l(e.authentication.password)
|
||||
}), {
|
||||
id: e._id,
|
||||
createdAt: new Date(e.created ?? Date.now()).toISOString().replace("Z", ""),
|
||||
updatedAt: new Date(e.updated ?? Date.now()).toISOString().replace("Z", ""),
|
||||
workspaceId: t,
|
||||
folderId: e.parentId === t ? null : e.parentId,
|
||||
model: "http_request",
|
||||
sortPriority: n,
|
||||
name: e.name,
|
||||
url: l(e.url),
|
||||
body: r,
|
||||
bodyType: a,
|
||||
authentication: u,
|
||||
authenticationType: i,
|
||||
method: e.method,
|
||||
headers: (e.headers ?? []).map(({ name: d, value: p, disabled: s }) => ({
|
||||
enabled: !s,
|
||||
name: d,
|
||||
value: p
|
||||
})).filter(({ name: d, value: p }) => d !== "" || p !== "")
|
||||
};
|
||||
}
|
||||
function _(e, t) {
|
||||
return (
|
||||
console.log('IMPORTING FOLDER', e._id, e.name, JSON.stringify(e, null, 2)),
|
||||
{
|
||||
id: e._id,
|
||||
createdAt: new Date(e.created ?? Date.now()).toISOString().replace('Z', ''),
|
||||
updatedAt: new Date(e.updated ?? Date.now()).toISOString().replace('Z', ''),
|
||||
folderId: e.parentId === t ? null : e.parentId,
|
||||
workspaceId: t,
|
||||
model: 'folder',
|
||||
name: e.name,
|
||||
}
|
||||
);
|
||||
return console.log("IMPORTING FOLDER", e._id, e.name, JSON.stringify(e, null, 2)), {
|
||||
id: e._id,
|
||||
createdAt: new Date(e.created ?? Date.now()).toISOString().replace("Z", ""),
|
||||
updatedAt: new Date(e.updated ?? Date.now()).toISOString().replace("Z", ""),
|
||||
folderId: e.parentId === t ? null : e.parentId,
|
||||
workspaceId: t,
|
||||
model: "folder",
|
||||
name: e.name
|
||||
};
|
||||
}
|
||||
function b(e) {
|
||||
console.log('RUNNING INSOMNIA');
|
||||
console.log("RUNNING INSOMNIA");
|
||||
let t;
|
||||
try {
|
||||
t = JSON.parse(e);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
if (!m(t) || !Array.isArray(t.resources)) return;
|
||||
if (!m(t) || !Array.isArray(t.resources))
|
||||
return;
|
||||
const n = {
|
||||
workspaces: [],
|
||||
requests: [],
|
||||
environments: [],
|
||||
folders: [],
|
||||
},
|
||||
a = t.resources.filter(I);
|
||||
workspaces: [],
|
||||
requests: [],
|
||||
environments: [],
|
||||
folders: []
|
||||
}, a = t.resources.filter(I);
|
||||
for (const r of a) {
|
||||
const i = t.resources.find((o) => f(o) && o.parentId === r._id);
|
||||
const i = t.resources.find(
|
||||
(o) => f(o) && o.parentId === r._id
|
||||
);
|
||||
n.workspaces.push({
|
||||
id: r._id,
|
||||
createdAt: new Date(a.created ?? Date.now()).toISOString().replace('Z', ''),
|
||||
updatedAt: new Date(a.updated ?? Date.now()).toISOString().replace('Z', ''),
|
||||
model: 'workspace',
|
||||
createdAt: new Date(a.created ?? Date.now()).toISOString().replace("Z", ""),
|
||||
updatedAt: new Date(a.updated ?? Date.now()).toISOString().replace("Z", ""),
|
||||
model: "workspace",
|
||||
name: r.name,
|
||||
variables: i ? O(i.data) : [],
|
||||
variables: i ? O(i.data) : []
|
||||
});
|
||||
const u = t.resources.filter((o) => f(o) && o.parentId === (i == null ? void 0 : i._id));
|
||||
n.environments.push(...u.map((o) => S(o, r._id)));
|
||||
const u = t.resources.filter(
|
||||
(o) => f(o) && o.parentId === (i == null ? void 0 : i._id)
|
||||
);
|
||||
n.environments.push(
|
||||
...u.map((o) => S(o, r._id))
|
||||
);
|
||||
const c = (o) => {
|
||||
const d = t.resources.filter((s) => s.parentId === o);
|
||||
let p = 0;
|
||||
@@ -142,11 +127,8 @@ function b(e) {
|
||||
};
|
||||
c(r._id);
|
||||
}
|
||||
return (
|
||||
(n.requests = n.requests.filter(Boolean)),
|
||||
(n.environments = n.environments.filter(Boolean)),
|
||||
(n.workspaces = n.workspaces.filter(Boolean)),
|
||||
{ resources: n }
|
||||
);
|
||||
return n.requests = n.requests.filter(Boolean), n.environments = n.environments.filter(Boolean), n.workspaces = n.workspaces.filter(Boolean), { resources: n };
|
||||
}
|
||||
export { b as pluginHookImport };
|
||||
export {
|
||||
b as pluginHookImport
|
||||
};
|
||||
|
||||
@@ -1,130 +1,160 @@
|
||||
const f = 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json',
|
||||
b = 'https://schema.getpostman.com/json/collection/v2.0.0/collection.json',
|
||||
w = [b, f];
|
||||
function A(t) {
|
||||
const e = m(t);
|
||||
if (e == null) return;
|
||||
const r = s(e.info);
|
||||
if (!w.includes(r.schema) || !Array.isArray(e.item)) return;
|
||||
const a = {
|
||||
workspaces: [],
|
||||
environments: [],
|
||||
requests: [],
|
||||
folders: [],
|
||||
},
|
||||
c = {
|
||||
model: 'workspace',
|
||||
id: 'wrk_0',
|
||||
name: r.name || 'Postman Import',
|
||||
description: r.description || '',
|
||||
};
|
||||
a.workspaces.push(c);
|
||||
const p = (o, l = null) => {
|
||||
if (typeof o.name == 'string' && Array.isArray(o.item)) {
|
||||
const n = {
|
||||
model: 'folder',
|
||||
workspaceId: c.id,
|
||||
id: `fld_${a.folders.length}`,
|
||||
name: o.name,
|
||||
folderId: l,
|
||||
};
|
||||
a.folders.push(n);
|
||||
for (const i of o.item) p(i, n.id);
|
||||
} else if (typeof o.name == 'string' && 'request' in o) {
|
||||
const n = s(o.request),
|
||||
i = T(n.body),
|
||||
u = g(n.auth),
|
||||
y = {
|
||||
model: 'http_request',
|
||||
id: `req_${a.requests.length}`,
|
||||
workspaceId: c.id,
|
||||
folderId: l,
|
||||
name: o.name,
|
||||
method: n.method || 'GET',
|
||||
url: typeof n.url == 'string' ? n.url : s(n.url).raw,
|
||||
body: i.body,
|
||||
bodyType: i.bodyType,
|
||||
authentication: u.authentication,
|
||||
authenticationType: u.authenticationType,
|
||||
headers: [
|
||||
...i.headers,
|
||||
...u.headers,
|
||||
...h(n.header).map((d) => ({
|
||||
name: d.key,
|
||||
value: d.value,
|
||||
enabled: !d.disabled,
|
||||
})),
|
||||
],
|
||||
};
|
||||
a.requests.push(y);
|
||||
} else console.log('Unknown item', o, l);
|
||||
const T = "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", w = "https://schema.getpostman.com/json/collection/v2.0.0/collection.json", A = [w, T];
|
||||
function q(e) {
|
||||
const t = b(e);
|
||||
if (t == null)
|
||||
return;
|
||||
const n = a(t.info);
|
||||
if (!A.includes(n.schema) || !Array.isArray(t.item))
|
||||
return;
|
||||
const i = {
|
||||
workspaces: [],
|
||||
environments: [],
|
||||
requests: [],
|
||||
folders: []
|
||||
}, c = {
|
||||
model: "workspace",
|
||||
id: m("wk"),
|
||||
name: n.name || "Postman Import",
|
||||
description: n.description || ""
|
||||
};
|
||||
for (const o of e.item) p(o);
|
||||
return { resources: a };
|
||||
}
|
||||
function g(t) {
|
||||
const e = s(t);
|
||||
return 'basic' in e
|
||||
? {
|
||||
headers: [],
|
||||
authenticationType: 'basic',
|
||||
authentication: {
|
||||
username: e.basic.username || '',
|
||||
password: e.basic.password || '',
|
||||
},
|
||||
}
|
||||
: { headers: [], authenticationType: null, authentication: {} };
|
||||
}
|
||||
function T(t) {
|
||||
const e = s(t);
|
||||
return 'graphql' in e
|
||||
? {
|
||||
i.workspaces.push(c);
|
||||
const f = (r, u = null) => {
|
||||
if (typeof r.name == "string" && Array.isArray(r.item)) {
|
||||
const o = {
|
||||
model: "folder",
|
||||
workspaceId: c.id,
|
||||
id: m("fl"),
|
||||
name: r.name,
|
||||
folderId: u
|
||||
};
|
||||
i.folders.push(o);
|
||||
for (const s of r.item)
|
||||
f(s, o.id);
|
||||
} else if (typeof r.name == "string" && "request" in r) {
|
||||
const o = a(r.request), s = k(o.body), d = S(o.auth), g = {
|
||||
model: "http_request",
|
||||
id: m("rq"),
|
||||
workspaceId: c.id,
|
||||
folderId: u,
|
||||
name: r.name,
|
||||
method: o.method || "GET",
|
||||
url: typeof o.url == "string" ? o.url : a(o.url).raw,
|
||||
body: s.body,
|
||||
bodyType: s.bodyType,
|
||||
authentication: d.authentication,
|
||||
authenticationType: d.authenticationType,
|
||||
headers: [
|
||||
{
|
||||
name: 'Content-Type',
|
||||
value: 'application/json',
|
||||
enabled: !0,
|
||||
},
|
||||
],
|
||||
bodyType: 'graphql',
|
||||
body: {
|
||||
text: JSON.stringify(
|
||||
{ query: e.graphql.query, variables: m(e.graphql.variables) },
|
||||
null,
|
||||
2,
|
||||
),
|
||||
},
|
||||
}
|
||||
: 'formdata' in e
|
||||
? {
|
||||
headers: [
|
||||
{
|
||||
name: 'Content-Type',
|
||||
value: 'application/x-www-form-urlencoded',
|
||||
enabled: !0,
|
||||
},
|
||||
],
|
||||
bodyType: 'application/x-www-form-urlencoded',
|
||||
body: {
|
||||
form: h(e.formdata).map((r) => ({
|
||||
enabled: !r.disabled,
|
||||
name: r.key ?? '',
|
||||
value: r.value ?? '',
|
||||
})),
|
||||
},
|
||||
}
|
||||
: { headers: [], bodyType: null, body: {} };
|
||||
...s.headers,
|
||||
...d.headers,
|
||||
...y(o.header).map((p) => ({
|
||||
name: p.key,
|
||||
value: p.value,
|
||||
enabled: !p.disabled
|
||||
}))
|
||||
]
|
||||
};
|
||||
i.requests.push(g);
|
||||
} else
|
||||
console.log("Unknown item", r, u);
|
||||
};
|
||||
for (const r of t.item)
|
||||
f(r);
|
||||
return { resources: h(i) };
|
||||
}
|
||||
function m(t) {
|
||||
function S(e) {
|
||||
const t = a(e);
|
||||
return "basic" in t ? {
|
||||
headers: [],
|
||||
authenticationType: "basic",
|
||||
authentication: {
|
||||
username: t.basic.username || "",
|
||||
password: t.basic.password || ""
|
||||
}
|
||||
} : { headers: [], authenticationType: null, authentication: {} };
|
||||
}
|
||||
function k(e) {
|
||||
const t = a(e);
|
||||
return "graphql" in t ? {
|
||||
headers: [
|
||||
{
|
||||
name: "Content-Type",
|
||||
value: "application/json",
|
||||
enabled: !0
|
||||
}
|
||||
],
|
||||
bodyType: "graphql",
|
||||
body: {
|
||||
text: JSON.stringify(
|
||||
{ query: t.graphql.query, variables: b(t.graphql.variables) },
|
||||
null,
|
||||
2
|
||||
)
|
||||
}
|
||||
} : "urlencoded" in t ? {
|
||||
headers: [
|
||||
{
|
||||
name: "Content-Type",
|
||||
value: "application/x-www-form-urlencoded",
|
||||
enabled: !0
|
||||
}
|
||||
],
|
||||
bodyType: "application/x-www-form-urlencoded",
|
||||
body: {
|
||||
form: y(t.urlencoded).map((n) => ({
|
||||
enabled: !n.disabled,
|
||||
name: n.key ?? "",
|
||||
value: n.value ?? ""
|
||||
}))
|
||||
}
|
||||
} : "formdata" in t ? {
|
||||
headers: [
|
||||
{
|
||||
name: "Content-Type",
|
||||
value: "multipart/form-data",
|
||||
enabled: !0
|
||||
}
|
||||
],
|
||||
bodyType: "multipart/form-data",
|
||||
body: {
|
||||
form: y(t.formdata).map(
|
||||
(n) => n.src != null ? {
|
||||
enabled: !n.disabled,
|
||||
name: n.key ?? "",
|
||||
file: n.src ?? ""
|
||||
} : {
|
||||
enabled: !n.disabled,
|
||||
name: n.key ?? "",
|
||||
value: n.value ?? ""
|
||||
}
|
||||
)
|
||||
}
|
||||
} : { headers: [], bodyType: null, body: {} };
|
||||
}
|
||||
function b(e) {
|
||||
try {
|
||||
return s(JSON.parse(t));
|
||||
} catch {}
|
||||
return a(JSON.parse(e));
|
||||
} catch {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function s(t) {
|
||||
return Object.prototype.toString.call(t) === '[object Object]' ? t : {};
|
||||
function a(e) {
|
||||
return Object.prototype.toString.call(e) === "[object Object]" ? e : {};
|
||||
}
|
||||
function h(t) {
|
||||
return Object.prototype.toString.call(t) === '[object Array]' ? t : [];
|
||||
function y(e) {
|
||||
return Object.prototype.toString.call(e) === "[object Array]" ? e : [];
|
||||
}
|
||||
export { A as pluginHookImport };
|
||||
function h(e) {
|
||||
return typeof e == "string" ? e.replace(/{{\s*(_\.)?([^}]+)\s*}}/g, "${[$2]}") : Array.isArray(e) && e != null ? e.map(h) : typeof e == "object" && e != null ? Object.fromEntries(
|
||||
Object.entries(e).map(([t, n]) => [t, h(n)])
|
||||
) : e;
|
||||
}
|
||||
function m(e) {
|
||||
const t = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
let n = `${e}_`;
|
||||
for (let l = 0; l < 10; l++)
|
||||
n += t[Math.floor(Math.random() * t.length)];
|
||||
return n;
|
||||
}
|
||||
export {
|
||||
q as pluginHookImport
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -32,7 +32,7 @@ export function pluginHookImport(contents: string): { resources: ExportResources
|
||||
|
||||
const workspace: ExportResources['workspaces'][0] = {
|
||||
model: 'workspace',
|
||||
id: 'wrk_0',
|
||||
id: generateId('wk'),
|
||||
name: info.name || 'Postman Import',
|
||||
description: info.description || '',
|
||||
};
|
||||
@@ -43,7 +43,7 @@ export function pluginHookImport(contents: string): { resources: ExportResources
|
||||
const folder: ExportResources['folders'][0] = {
|
||||
model: 'folder',
|
||||
workspaceId: workspace.id,
|
||||
id: `fld_${exportResources.folders.length}`,
|
||||
id: generateId('fl'),
|
||||
name: v.name,
|
||||
folderId,
|
||||
};
|
||||
@@ -57,7 +57,7 @@ export function pluginHookImport(contents: string): { resources: ExportResources
|
||||
const authPatch = importAuth(r.auth);
|
||||
const request: ExportResources['requests'][0] = {
|
||||
model: 'http_request',
|
||||
id: `req_${exportResources.requests.length}`,
|
||||
id: generateId('rq'),
|
||||
workspaceId: workspace.id,
|
||||
folderId,
|
||||
name: v.name,
|
||||
@@ -89,7 +89,7 @@ export function pluginHookImport(contents: string): { resources: ExportResources
|
||||
importItem(item);
|
||||
}
|
||||
|
||||
return { resources: exportResources };
|
||||
return { resources: convertTemplateSyntax(exportResources) };
|
||||
}
|
||||
|
||||
function importAuth(
|
||||
@@ -131,7 +131,7 @@ function importBody(rawBody: any): Pick<HttpRequest, 'body' | 'bodyType' | 'head
|
||||
),
|
||||
},
|
||||
};
|
||||
} else if ('formdata' in body) {
|
||||
} else if ('urlencoded' in body) {
|
||||
return {
|
||||
headers: [
|
||||
{
|
||||
@@ -142,13 +142,39 @@ function importBody(rawBody: any): Pick<HttpRequest, 'body' | 'bodyType' | 'head
|
||||
],
|
||||
bodyType: 'application/x-www-form-urlencoded',
|
||||
body: {
|
||||
form: toArray(body.formdata).map((f) => ({
|
||||
form: toArray(body.urlencoded).map((f) => ({
|
||||
enabled: !f.disabled,
|
||||
name: f.key ?? '',
|
||||
value: f.value ?? '',
|
||||
})),
|
||||
},
|
||||
};
|
||||
} else if ('formdata' in body) {
|
||||
return {
|
||||
headers: [
|
||||
{
|
||||
name: 'Content-Type',
|
||||
value: 'multipart/form-data',
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
bodyType: 'multipart/form-data',
|
||||
body: {
|
||||
form: toArray(body.formdata).map((f) =>
|
||||
f.src != null
|
||||
? {
|
||||
enabled: !f.disabled,
|
||||
name: f.key ?? '',
|
||||
file: f.src ?? '',
|
||||
}
|
||||
: {
|
||||
enabled: !f.disabled,
|
||||
name: f.key ?? '',
|
||||
value: f.value ?? '',
|
||||
},
|
||||
),
|
||||
},
|
||||
};
|
||||
} else {
|
||||
// TODO: support other body types
|
||||
return { headers: [], bodyType: null, body: {} };
|
||||
@@ -171,3 +197,27 @@ function toArray(value: any): any[] {
|
||||
if (Object.prototype.toString.call(value) === '[object Array]') return value;
|
||||
else return [];
|
||||
}
|
||||
|
||||
/** Recursively render all nested object properties */
|
||||
function convertTemplateSyntax<T>(obj: T): T {
|
||||
if (typeof obj === 'string') {
|
||||
return obj.replace(/{{\s*(_\.)?([^}]+)\s*}}/g, '${[$2]}') as T;
|
||||
} else if (Array.isArray(obj) && obj != null) {
|
||||
return obj.map(convertTemplateSyntax) as T;
|
||||
} else if (typeof obj === 'object' && obj != null) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(obj).map(([k, v]) => [k, convertTemplateSyntax(v)]),
|
||||
) as T;
|
||||
} else {
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
|
||||
function generateId(prefix: 'wk' | 'rq' | 'fl'): string {
|
||||
const alphabet = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
let id = `${prefix}_`;
|
||||
for (let i = 0; i < 10; i++) {
|
||||
id += alphabet[Math.floor(Math.random() * alphabet.length)];
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": [
|
||||
"ES2020"
|
||||
"ESNext",
|
||||
],
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
@@ -18,6 +18,6 @@
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
"./src"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -5,9 +5,13 @@ function u(r) {
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
if (t(e) && e.yaakSchema === 1) return { resources: e.resources };
|
||||
if (t(e) && e.yaakSchema === 1)
|
||||
return { resources: e.resources };
|
||||
}
|
||||
function t(r) {
|
||||
return Object.prototype.toString.call(r) === '[object Object]';
|
||||
return Object.prototype.toString.call(r) === "[object Object]";
|
||||
}
|
||||
export { t as isJSObject, u as pluginHookImport };
|
||||
export {
|
||||
t as isJSObject,
|
||||
u as pluginHookImport
|
||||
};
|
||||
|
||||
@@ -1,89 +1,109 @@
|
||||
use log::{debug, warn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::types::JsonValue;
|
||||
use tauri::{async_runtime, AppHandle, Manager};
|
||||
|
||||
use crate::is_dev;
|
||||
|
||||
// serializable
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub enum AnalyticsResource {
|
||||
App,
|
||||
// Workspace,
|
||||
// Environment,
|
||||
// Folder,
|
||||
// HttpRequest,
|
||||
// HttpResponse,
|
||||
Workspace,
|
||||
Environment,
|
||||
Folder,
|
||||
HttpRequest,
|
||||
HttpResponse,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub enum AnalyticsAction {
|
||||
Launch,
|
||||
// Create,
|
||||
// Update,
|
||||
// Upsert,
|
||||
// Delete,
|
||||
// Send,
|
||||
// Duplicate,
|
||||
Create,
|
||||
Update,
|
||||
Upsert,
|
||||
Delete,
|
||||
DeleteMany,
|
||||
Send,
|
||||
Duplicate,
|
||||
}
|
||||
|
||||
fn resource_name(resource: AnalyticsResource) -> &'static str {
|
||||
match resource {
|
||||
AnalyticsResource::App => "app",
|
||||
// AnalyticsResource::Workspace => "workspace",
|
||||
// AnalyticsResource::Environment => "environment",
|
||||
// AnalyticsResource::Folder => "folder",
|
||||
// AnalyticsResource::HttpRequest => "http_request",
|
||||
// AnalyticsResource::HttpResponse => "http_response",
|
||||
AnalyticsResource::Workspace => "workspace",
|
||||
AnalyticsResource::Environment => "environment",
|
||||
AnalyticsResource::Folder => "folder",
|
||||
AnalyticsResource::HttpRequest => "http_request",
|
||||
AnalyticsResource::HttpResponse => "http_response",
|
||||
}
|
||||
}
|
||||
|
||||
fn action_name(action: AnalyticsAction) -> &'static str {
|
||||
match action {
|
||||
AnalyticsAction::Launch => "launch",
|
||||
// AnalyticsAction::Create => "create",
|
||||
// AnalyticsAction::Update => "update",
|
||||
// AnalyticsAction::Upsert => "upsert",
|
||||
// AnalyticsAction::Delete => "delete",
|
||||
// AnalyticsAction::Send => "send",
|
||||
// AnalyticsAction::Duplicate => "duplicate",
|
||||
AnalyticsAction::Create => "create",
|
||||
AnalyticsAction::Update => "update",
|
||||
AnalyticsAction::Upsert => "upsert",
|
||||
AnalyticsAction::Delete => "delete",
|
||||
AnalyticsAction::DeleteMany => "delete_many",
|
||||
AnalyticsAction::Send => "send",
|
||||
AnalyticsAction::Duplicate => "duplicate",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn track_event(
|
||||
pub fn track_event_blocking(
|
||||
app_handle: &AppHandle,
|
||||
resource: AnalyticsResource,
|
||||
action: AnalyticsAction,
|
||||
attributes: Option<JsonValue>,
|
||||
) {
|
||||
async_runtime::block_on(async move {
|
||||
let event = format!("{}.{}", resource_name(resource), action_name(action));
|
||||
let attributes_json = attributes.unwrap_or("{}".to_string().into()).to_string();
|
||||
let info = app_handle.package_info();
|
||||
let tz = datetime::sys_timezone().unwrap_or("unknown".to_string());
|
||||
let params = vec![
|
||||
("e", event.clone()),
|
||||
("a", attributes_json.clone()),
|
||||
("id", "site_zOK0d7jeBy2TLxFCnZ".to_string()),
|
||||
("v", info.version.clone().to_string()),
|
||||
("os", get_os().to_string()),
|
||||
("tz", tz),
|
||||
("xy", get_window_size(app_handle)),
|
||||
];
|
||||
let url = "https://t.yaak.app/t/e".to_string();
|
||||
let req = reqwest::Client::builder()
|
||||
.build()
|
||||
.unwrap()
|
||||
.get(&url)
|
||||
.query(¶ms);
|
||||
track_event(app_handle, resource, action, attributes).await;
|
||||
});
|
||||
}
|
||||
|
||||
if is_dev() {
|
||||
debug!("Send event (dev): {}", event);
|
||||
} else if let Err(e) = req.send().await {
|
||||
warn!(
|
||||
pub async fn track_event(
|
||||
app_handle: &AppHandle,
|
||||
resource: AnalyticsResource,
|
||||
action: AnalyticsAction,
|
||||
attributes: Option<JsonValue>,
|
||||
) {
|
||||
let event = format!("{}.{}", resource_name(resource), action_name(action));
|
||||
let attributes_json = attributes.unwrap_or("{}".to_string().into()).to_string();
|
||||
let info = app_handle.package_info();
|
||||
let tz = datetime::sys_timezone().unwrap_or("unknown".to_string());
|
||||
let site = match is_dev() {
|
||||
true => "site_TkHWjoXwZPq3HfhERb",
|
||||
false => "site_zOK0d7jeBy2TLxFCnZ",
|
||||
};
|
||||
let base_url = match is_dev() {
|
||||
true => "http://localhost:7194",
|
||||
false => "https://t.yaak.app"
|
||||
};
|
||||
let params = vec![
|
||||
("e", event.clone()),
|
||||
("a", attributes_json.clone()),
|
||||
("id", site.to_string()),
|
||||
("v", info.version.clone().to_string()),
|
||||
("os", get_os().to_string()),
|
||||
("tz", tz),
|
||||
("xy", get_window_size(app_handle)),
|
||||
];
|
||||
let req = reqwest::Client::builder()
|
||||
.build()
|
||||
.unwrap()
|
||||
.get(format!("{base_url}/t/e"))
|
||||
.query(¶ms);
|
||||
|
||||
if let Err(e) = req.send().await {
|
||||
warn!(
|
||||
"Error sending analytics event: {} {} {:?}",
|
||||
e, event, params
|
||||
);
|
||||
} else {
|
||||
debug!("Send event: {}: {:?}", event, params);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
debug!("Send event: {}: {:?}", event, params);
|
||||
}
|
||||
}
|
||||
|
||||
fn get_os() -> &'static str {
|
||||
|
||||
@@ -13,23 +13,25 @@ use std::fs::{create_dir_all, File};
|
||||
use std::process::exit;
|
||||
|
||||
use fern::colors::ColoredLevelConfig;
|
||||
use log::{debug, error, info};
|
||||
use log::{debug, info, warn};
|
||||
use rand::random;
|
||||
use serde::Serialize;
|
||||
use sqlx::{Pool, Sqlite, SqlitePool};
|
||||
use serde_json::Value;
|
||||
use sqlx::migrate::Migrator;
|
||||
use sqlx::types::Json;
|
||||
use tauri::{AppHandle, Menu, RunEvent, State, Submenu, Window, WindowUrl, Wry};
|
||||
use tauri::{CustomMenuItem, Manager, WindowEvent};
|
||||
use sqlx::{Pool, Sqlite, SqlitePool};
|
||||
#[cfg(target_os = "macos")]
|
||||
use tauri::TitleBarStyle;
|
||||
use tauri::{AppHandle, RunEvent, State, Window, WindowUrl, Wry};
|
||||
use tauri::{Manager, WindowEvent};
|
||||
use tauri_plugin_log::{fern, LogTarget};
|
||||
use tauri_plugin_window_state::{StateFlags, WindowExt};
|
||||
use tokio::sync::Mutex;
|
||||
use window_shadows::set_shadow;
|
||||
|
||||
use window_ext::TrafficLightWindowExt;
|
||||
|
||||
use crate::analytics::{AnalyticsAction, AnalyticsResource, track_event};
|
||||
use crate::analytics::{AnalyticsAction, AnalyticsResource};
|
||||
use crate::plugin::{ImportResources, ImportResult};
|
||||
use crate::send::actually_send_request;
|
||||
use crate::updates::{update_mode_from_str, UpdateMode, YaakUpdater};
|
||||
@@ -67,7 +69,7 @@ async fn migrate_db(
|
||||
info!("Running migrations at {}", p.to_string_lossy());
|
||||
let m = Migrator::new(p).await.expect("Failed to load migrations");
|
||||
m.run(pool).await.expect("Failed to run migrations");
|
||||
info!("Migrations complete");
|
||||
info!("Migrations complete!");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -186,7 +188,7 @@ async fn send_request(
|
||||
.await
|
||||
.expect("Failed to get request");
|
||||
|
||||
let response = models::create_response(&req.id, 0, "", 0, None, None, None, None, vec![], pool)
|
||||
let response = models::create_response(&req.id, 0, "", 0, None, None, None, vec![], pool)
|
||||
.await
|
||||
.expect("Failed to create response");
|
||||
|
||||
@@ -224,6 +226,17 @@ async fn response_err(
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn track_event(
|
||||
window: Window<Wry>,
|
||||
resource: AnalyticsResource,
|
||||
action: AnalyticsAction,
|
||||
attributes: Option<Value>,
|
||||
) -> Result<(), String> {
|
||||
analytics::track_event(&window.app_handle(), resource, action, attributes).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn set_update_mode(
|
||||
update_mode: &str,
|
||||
@@ -504,6 +517,31 @@ async fn list_environments(
|
||||
Ok(environments)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn get_settings(
|
||||
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
|
||||
) -> Result<models::Settings, String> {
|
||||
let pool = &*db_instance.lock().await;
|
||||
models::get_or_create_settings(pool)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn update_settings(
|
||||
settings: models::Settings,
|
||||
window: Window<Wry>,
|
||||
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
|
||||
) -> Result<models::Settings, String> {
|
||||
let pool = &*db_instance.lock().await;
|
||||
|
||||
let updated_settings = models::update_settings(pool, settings)
|
||||
.await
|
||||
.expect("Failed to update settings");
|
||||
|
||||
emit_and_return(&window, "updated_model", updated_settings)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn get_folder(
|
||||
id: &str,
|
||||
@@ -551,10 +589,11 @@ async fn get_workspace(
|
||||
#[tauri::command]
|
||||
async fn list_responses(
|
||||
request_id: &str,
|
||||
limit: Option<i64>,
|
||||
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
|
||||
) -> Result<Vec<models::HttpResponse>, String> {
|
||||
let pool = &*db_instance.lock().await;
|
||||
models::find_responses(request_id, pool)
|
||||
models::find_responses(request_id, limit, pool)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
@@ -657,15 +696,27 @@ fn main() {
|
||||
.build(),
|
||||
)
|
||||
.plugin(tauri_plugin_window_state::Builder::default().build())
|
||||
.plugin(tauri_plugin_context_menu::init())
|
||||
.setup(|app| {
|
||||
let app_data_dir = app.path_resolver().app_data_dir().unwrap();
|
||||
let app_config_dir = app.path_resolver().app_config_dir().unwrap();
|
||||
info!(
|
||||
"App Config Dir: {}",
|
||||
app_config_dir.as_path().to_string_lossy(),
|
||||
);
|
||||
info!("App Data Dir: {}", app_data_dir.as_path().to_string_lossy(),);
|
||||
let dir = match is_dev() {
|
||||
true => current_dir().unwrap(),
|
||||
false => app.path_resolver().app_data_dir().unwrap(),
|
||||
false => app_data_dir,
|
||||
};
|
||||
|
||||
create_dir_all(dir.clone()).expect("Problem creating App directory!");
|
||||
let p = dir.join("db.sqlite");
|
||||
File::options()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.open(&p)
|
||||
.expect("Problem creating database file!");
|
||||
|
||||
let p_string = p.to_string_lossy().replace(' ', "%20");
|
||||
let url = format!("sqlite://{}?mode=rwc", p_string);
|
||||
println!("Connecting to database at {}", url);
|
||||
@@ -708,6 +759,7 @@ fn main() {
|
||||
get_environment,
|
||||
get_folder,
|
||||
get_request,
|
||||
get_settings,
|
||||
get_workspace,
|
||||
import_data,
|
||||
list_environments,
|
||||
@@ -720,9 +772,11 @@ fn main() {
|
||||
send_request,
|
||||
set_key_value,
|
||||
set_update_mode,
|
||||
track_event,
|
||||
update_environment,
|
||||
update_folder,
|
||||
update_request,
|
||||
update_settings,
|
||||
update_workspace,
|
||||
])
|
||||
.build(tauri::generate_context!())
|
||||
@@ -747,7 +801,7 @@ fn main() {
|
||||
debug!("Updater downloaded");
|
||||
}
|
||||
tauri::UpdaterEvent::Error(e) => {
|
||||
error!("Updater error: {:?}", e);
|
||||
warn!("Updater received error: {:?}", e);
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
@@ -756,7 +810,7 @@ fn main() {
|
||||
w.restore_state(StateFlags::all())
|
||||
.expect("Failed to restore window state");
|
||||
|
||||
track_event(
|
||||
analytics::track_event_blocking(
|
||||
app_handle,
|
||||
AnalyticsResource::App,
|
||||
AnalyticsAction::Launch,
|
||||
@@ -784,28 +838,18 @@ fn main() {
|
||||
}
|
||||
|
||||
fn is_dev() -> bool {
|
||||
let env = option_env!("YAAK_ENV");
|
||||
env.unwrap_or("production") != "production"
|
||||
#[cfg(dev)]
|
||||
{
|
||||
return true;
|
||||
}
|
||||
#[cfg(not(dev))]
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
fn create_window(handle: &AppHandle<Wry>, url: Option<&str>) -> Window<Wry> {
|
||||
let mut app_menu = window_menu::os_default("Yaak".to_string().as_str());
|
||||
if is_dev() {
|
||||
let submenu = Submenu::new(
|
||||
"Developer",
|
||||
Menu::new()
|
||||
.add_item(
|
||||
CustomMenuItem::new("refresh".to_string(), "Refresh")
|
||||
.accelerator("CmdOrCtrl + Shift + r"),
|
||||
)
|
||||
.add_item(
|
||||
CustomMenuItem::new("toggle_devtools".to_string(), "Open Devtools")
|
||||
.accelerator("CmdOrCtrl + Option + i"),
|
||||
),
|
||||
);
|
||||
app_menu = app_menu.add_submenu(submenu);
|
||||
}
|
||||
|
||||
let app_menu = window_menu::os_default("Yaak".to_string().as_str());
|
||||
let window_num = handle.windows().len();
|
||||
let window_id = format!("wnd_{}", window_num);
|
||||
let mut win_builder = tauri::WindowBuilder::new(
|
||||
@@ -813,7 +857,6 @@ fn create_window(handle: &AppHandle<Wry>, url: Option<&str>) -> Window<Wry> {
|
||||
window_id,
|
||||
WindowUrl::App(url.unwrap_or_default().into()),
|
||||
)
|
||||
.menu(app_menu)
|
||||
.fullscreen(false)
|
||||
.resizable(true)
|
||||
.inner_size(1100.0, 600.0)
|
||||
@@ -828,12 +871,24 @@ fn create_window(handle: &AppHandle<Wry>, url: Option<&str>) -> Window<Wry> {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
win_builder = win_builder
|
||||
.menu(app_menu)
|
||||
.hidden_title(true)
|
||||
.title_bar_style(TitleBarStyle::Overlay);
|
||||
}
|
||||
|
||||
// Add non-MacOS things
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
// Doesn't seem to work from Rust, here, so we do it in JS
|
||||
win_builder = win_builder.decorations(false);
|
||||
}
|
||||
|
||||
let win = win_builder.build().expect("failed to build window");
|
||||
|
||||
// Tauri doesn't support shadows when hiding decorations, so we add our own
|
||||
#[cfg(any(windows, target_os = "macos"))]
|
||||
set_shadow(&win, true).unwrap();
|
||||
|
||||
let win2 = win.clone();
|
||||
let handle2 = handle.clone();
|
||||
win.on_menu_event(move |event| match event.menu_item_id() {
|
||||
@@ -900,8 +955,7 @@ fn emit_side_effect<S: Serialize + Clone>(app_handle: &AppHandle<Wry>, event: &s
|
||||
}
|
||||
|
||||
async fn get_update_mode(pool: &Pool<Sqlite>) -> UpdateMode {
|
||||
let mode = models::get_key_value_string("app", "update_mode", pool)
|
||||
.await;
|
||||
let mode = models::get_key_value_string("app", "update_mode", pool).await;
|
||||
match mode {
|
||||
Some(mode) => update_mode_from_str(&mode),
|
||||
None => UpdateMode::Stable,
|
||||
|
||||
@@ -3,11 +3,27 @@ use std::fs;
|
||||
|
||||
use rand::distributions::{Alphanumeric, DistString};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{Pool, Sqlite};
|
||||
use sqlx::types::{Json, JsonValue};
|
||||
use sqlx::types::chrono::NaiveDateTime;
|
||||
use sqlx::types::{Json, JsonValue};
|
||||
use sqlx::{Pool, Sqlite};
|
||||
use tauri::AppHandle;
|
||||
|
||||
#[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Default)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct Settings {
|
||||
pub id: String,
|
||||
pub model: String,
|
||||
pub created_at: NaiveDateTime,
|
||||
pub updated_at: NaiveDateTime,
|
||||
|
||||
// Settings
|
||||
pub validate_certificates: bool,
|
||||
pub follow_redirects: bool,
|
||||
pub theme: String,
|
||||
pub appearance: String,
|
||||
pub request_timeout: i64,
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Default)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct Workspace {
|
||||
@@ -124,7 +140,6 @@ pub struct HttpResponse {
|
||||
pub elapsed: i64,
|
||||
pub status: i64,
|
||||
pub status_reason: Option<String>,
|
||||
pub body: Option<Vec<u8>>,
|
||||
pub body_path: Option<String>,
|
||||
pub headers: Json<Vec<HttpResponseHeader>>,
|
||||
}
|
||||
@@ -193,7 +208,11 @@ pub async fn get_key_value(namespace: &str, key: &str, pool: &Pool<Sqlite>) -> O
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub async fn get_key_value_string(namespace: &str, key: &str, pool: &Pool<Sqlite>) -> Option<String> {
|
||||
pub async fn get_key_value_string(
|
||||
namespace: &str,
|
||||
key: &str,
|
||||
pool: &Pool<Sqlite>,
|
||||
) -> Option<String> {
|
||||
let kv = get_key_value(namespace, key, pool).await?;
|
||||
let result = serde_json::from_str(&kv.value);
|
||||
match result {
|
||||
@@ -284,6 +303,68 @@ pub async fn delete_environment(id: &str, pool: &Pool<Sqlite>) -> Result<Environ
|
||||
Ok(env)
|
||||
}
|
||||
|
||||
async fn get_settings(pool: &Pool<Sqlite>) -> Result<Settings, sqlx::Error> {
|
||||
sqlx::query_as!(
|
||||
Settings,
|
||||
r#"
|
||||
SELECT
|
||||
id,
|
||||
model,
|
||||
created_at,
|
||||
updated_at,
|
||||
follow_redirects,
|
||||
validate_certificates,
|
||||
request_timeout,
|
||||
theme,
|
||||
appearance
|
||||
FROM settings
|
||||
WHERE id = 'default'
|
||||
"#,
|
||||
)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_or_create_settings(pool: &Pool<Sqlite>) -> Result<Settings, sqlx::Error> {
|
||||
let existing = get_settings(pool).await;
|
||||
if let Ok(s) = existing {
|
||||
Ok(s)
|
||||
} else {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO settings (id)
|
||||
VALUES ('default')
|
||||
"#,
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
get_settings(pool).await
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update_settings(
|
||||
pool: &Pool<Sqlite>,
|
||||
settings: Settings,
|
||||
) -> Result<Settings, sqlx::Error> {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
UPDATE settings SET (
|
||||
follow_redirects,
|
||||
validate_certificates,
|
||||
theme,
|
||||
appearance
|
||||
) = (?, ?, ?, ?) WHERE id = 'default';
|
||||
"#,
|
||||
settings.follow_redirects,
|
||||
settings.validate_certificates,
|
||||
settings.theme,
|
||||
settings.appearance,
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
get_settings(pool).await
|
||||
}
|
||||
|
||||
pub async fn upsert_environment(
|
||||
pool: &Pool<Sqlite>,
|
||||
environment: Environment,
|
||||
@@ -594,7 +675,6 @@ pub async fn create_response(
|
||||
status: i64,
|
||||
status_reason: Option<&str>,
|
||||
content_length: Option<i64>,
|
||||
body: Option<Vec<u8>>,
|
||||
body_path: Option<&str>,
|
||||
headers: Vec<HttpResponseHeader>,
|
||||
pool: &Pool<Sqlite>,
|
||||
@@ -613,11 +693,10 @@ pub async fn create_response(
|
||||
status,
|
||||
status_reason,
|
||||
content_length,
|
||||
body,
|
||||
body_path,
|
||||
headers
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
|
||||
"#,
|
||||
id,
|
||||
request_id,
|
||||
@@ -627,7 +706,6 @@ pub async fn create_response(
|
||||
status,
|
||||
status_reason,
|
||||
content_length,
|
||||
body,
|
||||
body_path,
|
||||
headers_json,
|
||||
)
|
||||
@@ -704,19 +782,17 @@ pub async fn update_response(
|
||||
status,
|
||||
status_reason,
|
||||
content_length,
|
||||
body,
|
||||
body_path,
|
||||
error,
|
||||
headers,
|
||||
updated_at
|
||||
) = (?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) WHERE id = ?;
|
||||
) = (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) WHERE id = ?;
|
||||
"#,
|
||||
response.elapsed,
|
||||
response.url,
|
||||
response.status,
|
||||
response.status_reason,
|
||||
response.content_length,
|
||||
response.body,
|
||||
response.body_path,
|
||||
response.error,
|
||||
headers_json,
|
||||
@@ -732,7 +808,7 @@ pub async fn get_response(id: &str, pool: &Pool<Sqlite>) -> Result<HttpResponse,
|
||||
HttpResponse,
|
||||
r#"
|
||||
SELECT id, model, workspace_id, request_id, updated_at, created_at, url,
|
||||
status, status_reason, content_length, body, body_path, elapsed, error,
|
||||
status, status_reason, content_length, body_path, elapsed, error,
|
||||
headers AS "headers!: sqlx::types::Json<Vec<HttpResponseHeader>>"
|
||||
FROM http_responses
|
||||
WHERE id = ?
|
||||
@@ -745,19 +821,26 @@ pub async fn get_response(id: &str, pool: &Pool<Sqlite>) -> Result<HttpResponse,
|
||||
|
||||
pub async fn find_responses(
|
||||
request_id: &str,
|
||||
limit: Option<i64>,
|
||||
pool: &Pool<Sqlite>,
|
||||
) -> Result<Vec<HttpResponse>, sqlx::Error> {
|
||||
let limit_unwrapped = match limit {
|
||||
Some(l) => l,
|
||||
None => i64::MAX,
|
||||
};
|
||||
sqlx::query_as!(
|
||||
HttpResponse,
|
||||
r#"
|
||||
SELECT id, model, workspace_id, request_id, updated_at, created_at, url,
|
||||
status, status_reason, content_length, body, body_path, elapsed, error,
|
||||
status, status_reason, content_length, body_path, elapsed, error,
|
||||
headers AS "headers!: sqlx::types::Json<Vec<HttpResponseHeader>>"
|
||||
FROM http_responses
|
||||
WHERE request_id = ?
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ?
|
||||
"#,
|
||||
request_id,
|
||||
limit_unwrapped,
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
@@ -771,7 +854,7 @@ pub async fn find_responses_by_workspace_id(
|
||||
HttpResponse,
|
||||
r#"
|
||||
SELECT id, model, workspace_id, request_id, updated_at, created_at, url,
|
||||
status, status_reason, content_length, body, body_path, elapsed, error,
|
||||
status, status_reason, content_length, body_path, elapsed, error,
|
||||
headers AS "headers!: sqlx::types::Json<Vec<HttpResponseHeader>>"
|
||||
FROM http_responses
|
||||
WHERE workspace_id = ?
|
||||
@@ -810,7 +893,7 @@ pub async fn delete_all_responses(
|
||||
request_id: &str,
|
||||
pool: &Pool<Sqlite>,
|
||||
) -> Result<(), sqlx::Error> {
|
||||
for r in find_responses(request_id, pool).await? {
|
||||
for r in find_responses(request_id, None, pool).await? {
|
||||
delete_response(&r.id, pool).await?;
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
use std::fs;
|
||||
use std::fs::{create_dir_all, File};
|
||||
use std::io::Write;
|
||||
use std::time::Duration;
|
||||
|
||||
use base64::Engine;
|
||||
use http::{HeaderMap, HeaderName, HeaderValue, Method};
|
||||
use http::header::{ACCEPT, USER_AGENT};
|
||||
use http::{HeaderMap, HeaderName, HeaderValue, Method};
|
||||
use log::warn;
|
||||
use reqwest::multipart;
|
||||
use reqwest::redirect::Policy;
|
||||
use sqlx::{Pool, Sqlite};
|
||||
use sqlx::types::Json;
|
||||
use sqlx::{Pool, Sqlite};
|
||||
use tauri::{AppHandle, Wry};
|
||||
|
||||
use crate::{emit_side_effect, models, render, response_err};
|
||||
@@ -34,11 +35,31 @@ pub async fn actually_send_request(
|
||||
url_string = format!("http://{}", url_string);
|
||||
}
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
.redirect(Policy::none())
|
||||
// .danger_accept_invalid_certs(true)
|
||||
.build()
|
||||
.expect("Failed to build client");
|
||||
let settings = models::get_or_create_settings(pool)
|
||||
.await
|
||||
.expect("Failed to get settings");
|
||||
|
||||
let mut client_builder = reqwest::Client::builder()
|
||||
.redirect(match settings.follow_redirects {
|
||||
true => Policy::limited(10), // TODO: Handle redirects natively
|
||||
false => Policy::none(),
|
||||
})
|
||||
.gzip(true)
|
||||
.brotli(true)
|
||||
.deflate(true)
|
||||
.referer(false)
|
||||
.danger_accept_invalid_certs(!settings.validate_certificates)
|
||||
.connection_verbose(true) // TODO: Capture this log somehow
|
||||
.tls_info(true);
|
||||
|
||||
if settings.request_timeout > 0 {
|
||||
client_builder = client_builder.timeout(Duration::from_millis(
|
||||
settings.request_timeout.unsigned_abs(),
|
||||
));
|
||||
}
|
||||
|
||||
// .use_rustls_tls() // TODO: Make this configurable (maybe)
|
||||
let client = client_builder.build().expect("Failed to build client");
|
||||
|
||||
let m = Method::from_bytes(request.method.to_uppercase().as_bytes())
|
||||
.expect("Failed to create method");
|
||||
@@ -114,7 +135,9 @@ pub async fn actually_send_request(
|
||||
|
||||
let mut query_params = Vec::new();
|
||||
for p in request.url_parameters.0 {
|
||||
if !p.enabled || p.name.is_empty() { continue; }
|
||||
if !p.enabled || p.name.is_empty() {
|
||||
continue;
|
||||
}
|
||||
query_params.push((
|
||||
render::render(&p.name, &workspace, environment_ref),
|
||||
render::render(&p.value, &workspace, environment_ref),
|
||||
@@ -128,18 +151,38 @@ pub async fn actually_send_request(
|
||||
let request_body = request.body.0;
|
||||
|
||||
if request_body.contains_key("text") {
|
||||
let raw_text = request_body.get("text").unwrap_or(empty_string).as_str().unwrap_or("");
|
||||
let raw_text = request_body
|
||||
.get("text")
|
||||
.unwrap_or(empty_string)
|
||||
.as_str()
|
||||
.unwrap_or("");
|
||||
let body = render::render(raw_text, &workspace, environment_ref);
|
||||
request_builder = request_builder.body(body);
|
||||
} else if body_type == "application/x-www-form-urlencoded" && request_body.contains_key("form") {
|
||||
} else if body_type == "application/x-www-form-urlencoded"
|
||||
&& request_body.contains_key("form")
|
||||
{
|
||||
let mut form_params = Vec::new();
|
||||
let form = request_body.get("form");
|
||||
if let Some(f) = form {
|
||||
for p in f.as_array().unwrap_or(&Vec::new()) {
|
||||
let enabled = p.get("enabled").unwrap_or(empty_bool).as_bool().unwrap_or(false);
|
||||
let name = p.get("name").unwrap_or(empty_string).as_str().unwrap_or_default();
|
||||
if !enabled || name.is_empty() { continue; }
|
||||
let value = p.get("value").unwrap_or(empty_string).as_str().unwrap_or_default();
|
||||
let enabled = p
|
||||
.get("enabled")
|
||||
.unwrap_or(empty_bool)
|
||||
.as_bool()
|
||||
.unwrap_or(false);
|
||||
let name = p
|
||||
.get("name")
|
||||
.unwrap_or(empty_string)
|
||||
.as_str()
|
||||
.unwrap_or_default();
|
||||
if !enabled || name.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let value = p
|
||||
.get("value")
|
||||
.unwrap_or(empty_string)
|
||||
.as_str()
|
||||
.unwrap_or_default();
|
||||
form_params.push((
|
||||
render::render(name, &workspace, environment_ref),
|
||||
render::render(value, &workspace, environment_ref),
|
||||
@@ -151,17 +194,41 @@ pub async fn actually_send_request(
|
||||
let mut multipart_form = multipart::Form::new();
|
||||
if let Some(form_definition) = request_body.get("form") {
|
||||
for p in form_definition.as_array().unwrap_or(&Vec::new()) {
|
||||
let enabled = p.get("enabled").unwrap_or(empty_bool).as_bool().unwrap_or(false);
|
||||
let name = p.get("name").unwrap_or(empty_string).as_str().unwrap_or_default();
|
||||
if !enabled || name.is_empty() { continue; }
|
||||
let enabled = p
|
||||
.get("enabled")
|
||||
.unwrap_or(empty_bool)
|
||||
.as_bool()
|
||||
.unwrap_or(false);
|
||||
let name = p
|
||||
.get("name")
|
||||
.unwrap_or(empty_string)
|
||||
.as_str()
|
||||
.unwrap_or_default();
|
||||
if !enabled || name.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let file = p.get("file").unwrap_or(empty_string).as_str().unwrap_or_default();
|
||||
let value = p.get("value").unwrap_or(empty_string).as_str().unwrap_or_default();
|
||||
let file = p
|
||||
.get("file")
|
||||
.unwrap_or(empty_string)
|
||||
.as_str()
|
||||
.unwrap_or_default();
|
||||
let value = p
|
||||
.get("value")
|
||||
.unwrap_or(empty_string)
|
||||
.as_str()
|
||||
.unwrap_or_default();
|
||||
multipart_form = multipart_form.part(
|
||||
render::render(name, &workspace, environment_ref),
|
||||
match !file.is_empty() {
|
||||
true => multipart::Part::bytes(fs::read(file).map_err(|e| e.to_string())?),
|
||||
false => multipart::Part::text(render::render(value, &workspace, environment_ref)),
|
||||
true => {
|
||||
multipart::Part::bytes(fs::read(file).map_err(|e| e.to_string())?)
|
||||
}
|
||||
false => multipart::Part::text(render::render(
|
||||
value,
|
||||
&workspace,
|
||||
environment_ref,
|
||||
)),
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -202,6 +269,7 @@ pub async fn actually_send_request(
|
||||
response.url = v.url().to_string();
|
||||
let body_bytes = v.bytes().await.expect("Failed to get body").to_vec();
|
||||
response.content_length = Some(body_bytes.len() as i64);
|
||||
println!("Response: {:?}", body_bytes.len());
|
||||
|
||||
{
|
||||
// Write body to FS
|
||||
@@ -228,11 +296,6 @@ pub async fn actually_send_request(
|
||||
);
|
||||
}
|
||||
|
||||
// Also store body directly on the model, if small enough
|
||||
if body_bytes.len() < 100_000 {
|
||||
response.body = Some(body_bytes);
|
||||
}
|
||||
|
||||
response.elapsed = start.elapsed().as_millis() as i64;
|
||||
response = models::update_response_if_id(&response, pool)
|
||||
.await
|
||||
@@ -242,6 +305,9 @@ pub async fn actually_send_request(
|
||||
}
|
||||
Ok(response)
|
||||
}
|
||||
Err(e) => response_err(response, e.to_string(), app_handle, pool).await,
|
||||
Err(e) => {
|
||||
println!("Yo: {}", e);
|
||||
response_err(response, e.to_string(), app_handle, pool).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use tauri::{AboutMetadata, CustomMenuItem, Menu, MenuItem, Submenu};
|
||||
use crate::is_dev;
|
||||
|
||||
pub fn os_default(#[allow(unused)] app_name: &str) -> Menu {
|
||||
let mut menu = Menu::new();
|
||||
@@ -12,6 +13,11 @@ pub fn os_default(#[allow(unused)] app_name: &str) -> Menu {
|
||||
AboutMetadata::default(),
|
||||
))
|
||||
.add_native_item(MenuItem::Separator)
|
||||
.add_item(
|
||||
CustomMenuItem::new("toggle_settings".to_string(), "Settings")
|
||||
.accelerator("CmdOrCtrl+,"),
|
||||
)
|
||||
.add_native_item(MenuItem::Separator)
|
||||
.add_native_item(MenuItem::Services)
|
||||
.add_native_item(MenuItem::Separator)
|
||||
.add_native_item(MenuItem::Hide)
|
||||
@@ -69,23 +75,23 @@ pub fn os_default(#[allow(unused)] app_name: &str) -> Menu {
|
||||
)
|
||||
.add_item(
|
||||
CustomMenuItem::new("zoom_out".to_string(), "Zoom Out").accelerator("CmdOrCtrl+-"),
|
||||
)
|
||||
.add_native_item(MenuItem::Separator)
|
||||
.add_item(
|
||||
CustomMenuItem::new("toggle_sidebar".to_string(), "Toggle Sidebar")
|
||||
.accelerator("CmdOrCtrl+b"),
|
||||
)
|
||||
.add_item(
|
||||
CustomMenuItem::new("focus_sidebar".to_string(), "Focus Sidebar")
|
||||
.accelerator("CmdOrCtrl+1"),
|
||||
)
|
||||
.add_item(
|
||||
CustomMenuItem::new("toggle_settings".to_string(), "Toggle Settings")
|
||||
.accelerator("CmdOrCtrl+,"),
|
||||
)
|
||||
.add_item(
|
||||
CustomMenuItem::new("focus_url".to_string(), "Focus URL").accelerator("CmdOrCtrl+l"),
|
||||
);
|
||||
// .add_native_item(MenuItem::Separator)
|
||||
// .add_item(
|
||||
// CustomMenuItem::new("toggle_sidebar".to_string(), "Toggle Sidebar")
|
||||
// .accelerator("CmdOrCtrl+b"),
|
||||
// )
|
||||
// .add_item(
|
||||
// CustomMenuItem::new("focus_sidebar".to_string(), "Focus Sidebar")
|
||||
// .accelerator("CmdOrCtrl+1"),
|
||||
// )
|
||||
// .add_item(
|
||||
// CustomMenuItem::new("toggle_settings".to_string(), "Toggle Settings")
|
||||
// .accelerator("CmdOrCtrl+,"),
|
||||
// )
|
||||
// .add_item(
|
||||
// CustomMenuItem::new("focus_url".to_string(), "Focus URL").accelerator("CmdOrCtrl+l"),
|
||||
// );
|
||||
menu = menu.add_submenu(Submenu::new("View", view_menu));
|
||||
|
||||
let mut window_menu = Menu::new();
|
||||
@@ -98,22 +104,37 @@ pub fn os_default(#[allow(unused)] app_name: &str) -> Menu {
|
||||
window_menu = window_menu.add_native_item(MenuItem::CloseWindow);
|
||||
menu = menu.add_submenu(Submenu::new("Window", window_menu));
|
||||
|
||||
menu = menu.add_submenu(Submenu::new(
|
||||
"Workspace",
|
||||
Menu::new()
|
||||
.add_item(
|
||||
CustomMenuItem::new("send_request".to_string(), "Send Request")
|
||||
.accelerator("CmdOrCtrl+r"),
|
||||
)
|
||||
.add_item(
|
||||
CustomMenuItem::new("new_request".to_string(), "New Request")
|
||||
.accelerator("CmdOrCtrl+n"),
|
||||
)
|
||||
.add_item(
|
||||
CustomMenuItem::new("duplicate_request".to_string(), "Duplicate Request")
|
||||
.accelerator("CmdOrCtrl+d"),
|
||||
),
|
||||
));
|
||||
// menu = menu.add_submenu(Submenu::new(
|
||||
// "Workspace",
|
||||
// Menu::new()
|
||||
// .add_item(
|
||||
// CustomMenuItem::new("send_request".to_string(), "Send Request")
|
||||
// .accelerator("CmdOrCtrl+r"),
|
||||
// )
|
||||
// .add_item(
|
||||
// CustomMenuItem::new("new_request".to_string(), "New Request")
|
||||
// .accelerator("CmdOrCtrl+n"),
|
||||
// )
|
||||
// .add_item(
|
||||
// CustomMenuItem::new("duplicate_request".to_string(), "Duplicate Request")
|
||||
// .accelerator("CmdOrCtrl+d"),
|
||||
// ),
|
||||
// ));
|
||||
|
||||
if is_dev() {
|
||||
menu = menu.add_submenu(Submenu::new(
|
||||
"Developer",
|
||||
Menu::new()
|
||||
.add_item(
|
||||
CustomMenuItem::new("refresh".to_string(), "Refresh")
|
||||
.accelerator("CmdOrCtrl + Shift + r"),
|
||||
)
|
||||
.add_item(
|
||||
CustomMenuItem::new("toggle_devtools".to_string(), "Open Devtools")
|
||||
.accelerator("CmdOrCtrl + Option + i"),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
menu
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
},
|
||||
"package": {
|
||||
"productName": "Yaak",
|
||||
"version": "2023.3.0"
|
||||
"version": "2024.0.1-beta.2"
|
||||
},
|
||||
"tauri": {
|
||||
"windows": [],
|
||||
@@ -35,8 +35,13 @@
|
||||
"open": true
|
||||
},
|
||||
"window": {
|
||||
"close": true,
|
||||
"maximize": true,
|
||||
"minimize": true,
|
||||
"setDecorations": true,
|
||||
"setTitle": true,
|
||||
"startDragging": true,
|
||||
"setTitle": true
|
||||
"unmaximize": true
|
||||
},
|
||||
"dialog": {
|
||||
"all": false,
|
||||
@@ -56,7 +61,7 @@
|
||||
"icons/release/icon.icns",
|
||||
"icons/release/icon.ico"
|
||||
],
|
||||
"identifier": "co.schier.yaak",
|
||||
"identifier": "app.yaak.desktop",
|
||||
"longDescription": "The best cross-platform visual API client",
|
||||
"resources": [
|
||||
"migrations/*",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||
import { MotionConfig } from 'framer-motion';
|
||||
import { Suspense } from 'react';
|
||||
import { DndProvider } from 'react-dnd';
|
||||
@@ -26,7 +25,7 @@ export function App() {
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<Suspense>
|
||||
<AppRouter />
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
{/*<ReactQueryDevtools initialIsOpen={false} />*/}
|
||||
</Suspense>
|
||||
</DndProvider>
|
||||
</HelmetProvider>
|
||||
|
||||
@@ -16,6 +16,7 @@ interface State {
|
||||
|
||||
interface Actions {
|
||||
show: (d: DialogEntryOptionalId) => void;
|
||||
toggle: (d: DialogEntry) => void;
|
||||
hide: (id: string) => void;
|
||||
}
|
||||
|
||||
@@ -26,15 +27,20 @@ export const DialogProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const [dialogs, setDialogs] = useState<State['dialogs']>([]);
|
||||
const actions = useMemo<Actions>(
|
||||
() => ({
|
||||
show: ({ id: oid, ...props }: DialogEntryOptionalId) => {
|
||||
show({ id: oid, ...props }: DialogEntryOptionalId) {
|
||||
const id = oid ?? Math.random().toString(36).slice(2);
|
||||
setDialogs((a) => [...a.filter((d) => d.id !== id), { id, ...props }]);
|
||||
},
|
||||
toggle({ id: oid, ...props }: DialogEntryOptionalId) {
|
||||
const id = oid ?? Math.random().toString(36).slice(2);
|
||||
if (dialogs.some((d) => d.id === id)) this.hide(id);
|
||||
else this.show({ id, ...props });
|
||||
},
|
||||
hide: (id: string) => {
|
||||
setDialogs((a) => a.filter((d) => d.id !== id));
|
||||
},
|
||||
}),
|
||||
[],
|
||||
[dialogs],
|
||||
);
|
||||
|
||||
const state: State = {
|
||||
|
||||
@@ -2,6 +2,7 @@ import classNames from 'classnames';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useActiveEnvironment } from '../hooks/useActiveEnvironment';
|
||||
import { useAppRoutes } from '../hooks/useAppRoutes';
|
||||
import { useCreateEnvironment } from '../hooks/useCreateEnvironment';
|
||||
import { useEnvironments } from '../hooks/useEnvironments';
|
||||
import type { ButtonProps } from './core/Button';
|
||||
import { Button } from './core/Button';
|
||||
@@ -21,11 +22,13 @@ export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdo
|
||||
}: Props) {
|
||||
const environments = useEnvironments();
|
||||
const activeEnvironment = useActiveEnvironment();
|
||||
const createEnvironment = useCreateEnvironment();
|
||||
const dialog = useDialog();
|
||||
const routes = useAppRoutes();
|
||||
|
||||
const showEnvironmentDialog = useCallback(() => {
|
||||
dialog.show({
|
||||
dialog.toggle({
|
||||
id: 'environment-editor',
|
||||
title: 'Manage Environments',
|
||||
render: () => <EnvironmentEditDialog initialEnvironment={activeEnvironment} />,
|
||||
});
|
||||
@@ -51,14 +54,25 @@ export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdo
|
||||
...((environments.length > 0
|
||||
? [{ type: 'separator', label: 'Environments' }]
|
||||
: []) as DropdownItem[]),
|
||||
{
|
||||
key: 'edit',
|
||||
label: 'Manage Environments',
|
||||
leftSlot: <Icon icon="gear" />,
|
||||
onSelect: showEnvironmentDialog,
|
||||
},
|
||||
environments.length
|
||||
? {
|
||||
key: 'edit',
|
||||
label: 'Manage Environments',
|
||||
hotkeyAction: 'environmentEditor.toggle',
|
||||
leftSlot: <Icon icon="gear" />,
|
||||
onSelect: showEnvironmentDialog,
|
||||
}
|
||||
: {
|
||||
key: 'new',
|
||||
label: 'New Environment',
|
||||
leftSlot: <Icon icon="plus" />,
|
||||
onSelect: async () => {
|
||||
await createEnvironment.mutateAsync();
|
||||
showEnvironmentDialog();
|
||||
},
|
||||
},
|
||||
],
|
||||
[activeEnvironment, environments, routes, showEnvironmentDialog],
|
||||
[activeEnvironment?.id, createEnvironment, environments, routes, showEnvironmentDialog],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -43,6 +43,11 @@ export const EnvironmentEditDialog = function ({ initialEnvironment }: Props) {
|
||||
[environments, selectedEnvironmentId],
|
||||
);
|
||||
|
||||
const handleCreateEnvironment = async () => {
|
||||
const e = await createEnvironment.mutateAsync();
|
||||
setSelectedEnvironmentId(e.id);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
@@ -53,30 +58,22 @@ export const EnvironmentEditDialog = function ({ initialEnvironment }: Props) {
|
||||
{showSidebar && (
|
||||
<aside className="grid grid-rows-[minmax(0,1fr)_auto] gap-y-0.5 h-full max-w-[250px] pr-3 border-r border-gray-100 -ml-2">
|
||||
<div className="min-w-0 h-full w-full overflow-y-scroll">
|
||||
<SidebarButton
|
||||
active={selectedEnvironment == null}
|
||||
onClick={() => setSelectedEnvironmentId(null)}
|
||||
>
|
||||
Base Environment
|
||||
</SidebarButton>
|
||||
<div className="ml-3 pl-2 border-l border-highlight">
|
||||
{environments.map((e) => (
|
||||
<SidebarButton
|
||||
key={e.id}
|
||||
active={selectedEnvironment?.id === e.id}
|
||||
onClick={() => setSelectedEnvironmentId(e.id)}
|
||||
>
|
||||
{e.name}
|
||||
</SidebarButton>
|
||||
))}
|
||||
</div>
|
||||
{environments.map((e) => (
|
||||
<SidebarButton
|
||||
key={e.id}
|
||||
active={selectedEnvironment?.id === e.id}
|
||||
onClick={() => setSelectedEnvironmentId(e.id)}
|
||||
>
|
||||
{e.name}
|
||||
</SidebarButton>
|
||||
))}
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
className="w-full text-center"
|
||||
color="gray"
|
||||
justify="center"
|
||||
onClick={() => createEnvironment.mutate()}
|
||||
onClick={handleCreateEnvironment}
|
||||
>
|
||||
New Environment
|
||||
</Button>
|
||||
@@ -191,7 +188,12 @@ const EnvironmentEditor = function ({
|
||||
<h1 className="text-xl">{environment?.name ?? 'Base Environment'}</h1>
|
||||
{items != null && (
|
||||
<Dropdown items={items}>
|
||||
<IconButton icon="gear" title="Environment Actions" size="sm" className="!h-auto w-8" />
|
||||
<IconButton
|
||||
icon="dotsV"
|
||||
title="Environment Actions"
|
||||
size="sm"
|
||||
className="!h-auto w-8"
|
||||
/>
|
||||
</Dropdown>
|
||||
)}
|
||||
</HStack>
|
||||
@@ -199,7 +201,6 @@ const EnvironmentEditor = function ({
|
||||
nameAutocomplete={nameAutocomplete}
|
||||
nameAutocompleteVariables={false}
|
||||
namePlaceholder="VAR_NAME"
|
||||
valuePlaceholder="variable value"
|
||||
nameValidate={validateName}
|
||||
valueAutocompleteVariables={false}
|
||||
forceUpdateKey={environment?.id ?? workspace?.id ?? 'n/a'}
|
||||
|
||||
@@ -2,7 +2,6 @@ import { useQueryClient } from '@tanstack/react-query';
|
||||
import { appWindow } from '@tauri-apps/api/window';
|
||||
import { useEffect } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useEffectOnce } from 'react-use';
|
||||
import { keyValueQueryKey } from '../hooks/useKeyValue';
|
||||
import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
|
||||
import { useRecentEnvironments } from '../hooks/useRecentEnvironments';
|
||||
@@ -11,9 +10,9 @@ import { useRecentWorkspaces } from '../hooks/useRecentWorkspaces';
|
||||
import { requestsQueryKey } from '../hooks/useRequests';
|
||||
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
|
||||
import { responsesQueryKey } from '../hooks/useResponses';
|
||||
import { settingsQueryKey } from '../hooks/useSettings';
|
||||
import { useSyncWindowTitle } from '../hooks/useSyncWindowTitle';
|
||||
import { workspacesQueryKey } from '../hooks/useWorkspaces';
|
||||
import { trackPage } from '../lib/analytics';
|
||||
import { NAMESPACE_NO_SYNC } from '../lib/keyValueStore';
|
||||
import type { HttpRequest, HttpResponse, Model, Workspace } from '../lib/models';
|
||||
import { modelsEq } from '../lib/models';
|
||||
@@ -39,10 +38,6 @@ export function GlobalHooks() {
|
||||
setPathname(location.pathname).catch(console.error);
|
||||
}, [location.pathname]);
|
||||
|
||||
useEffectOnce(() => {
|
||||
trackPage('/');
|
||||
});
|
||||
|
||||
useListenToTauriEvent<Model>('created_model', ({ payload, windowLabel }) => {
|
||||
if (shouldIgnoreEvent(payload, windowLabel)) return;
|
||||
|
||||
@@ -55,6 +50,8 @@ export function GlobalHooks() {
|
||||
? workspacesQueryKey(payload)
|
||||
: payload.model === 'key_value'
|
||||
? keyValueQueryKey(payload)
|
||||
: payload.model === 'settings'
|
||||
? settingsQueryKey()
|
||||
: null;
|
||||
|
||||
if (queryKey === null) {
|
||||
@@ -80,6 +77,8 @@ export function GlobalHooks() {
|
||||
? workspacesQueryKey(payload)
|
||||
: payload.model === 'key_value'
|
||||
? keyValueQueryKey(payload)
|
||||
: payload.model === 'settings'
|
||||
? settingsQueryKey()
|
||||
: null;
|
||||
|
||||
if (queryKey === null) {
|
||||
@@ -92,9 +91,11 @@ export function GlobalHooks() {
|
||||
}
|
||||
|
||||
if (!shouldIgnoreModel(payload)) {
|
||||
console.time('set query date');
|
||||
queryClient.setQueryData<Model[]>(queryKey, (values) =>
|
||||
values?.map((v) => (modelsEq(v, payload) ? payload : v)),
|
||||
);
|
||||
console.timeEnd('set query date');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -111,6 +112,8 @@ export function GlobalHooks() {
|
||||
queryClient.setQueryData<HttpResponse[]>(responsesQueryKey(payload), removeById(payload));
|
||||
} else if (payload.model === 'key_value') {
|
||||
queryClient.setQueryData(keyValueQueryKey(payload), undefined);
|
||||
} else if (payload.model === 'settings') {
|
||||
queryClient.setQueryData(settingsQueryKey(), undefined);
|
||||
}
|
||||
});
|
||||
useListenToTauriEvent<number>('zoom', ({ payload: zoomDelta, windowLabel }) => {
|
||||
|
||||
10
src-web/components/KeyboardShortcutsDialog.tsx
Normal file
10
src-web/components/KeyboardShortcutsDialog.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { hotkeyActions } from '../hooks/useHotKey';
|
||||
import { HotKeyList } from './core/HotKeyList';
|
||||
|
||||
export const KeyboardShortcutsDialog = () => {
|
||||
return (
|
||||
<div className="h-full w-full">
|
||||
<HotKeyList hotkeys={hotkeyActions} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -5,6 +5,7 @@ import { useActiveEnvironmentId } from '../hooks/useActiveEnvironmentId';
|
||||
import { useActiveRequest } from '../hooks/useActiveRequest';
|
||||
import { useActiveWorkspaceId } from '../hooks/useActiveWorkspaceId';
|
||||
import { useAppRoutes } from '../hooks/useAppRoutes';
|
||||
import { useHotKey } from '../hooks/useHotKey';
|
||||
import { useRecentRequests } from '../hooks/useRecentRequests';
|
||||
import { useRequests } from '../hooks/useRequests';
|
||||
import { fallbackRequestName } from '../lib/fallbackRequestName';
|
||||
@@ -33,25 +34,19 @@ export function RecentRequestsDropdown({ className }: Pick<ButtonProps, 'classNa
|
||||
|
||||
// Handle key-up
|
||||
useKeyPressEvent('Control', undefined, () => {
|
||||
if (!dropdownRef.current?.isOpen) return;
|
||||
dropdownRef.current?.select?.();
|
||||
});
|
||||
|
||||
useKey(
|
||||
'Tab',
|
||||
(e) => {
|
||||
if (!e.ctrlKey || recentRequestIds.length === 0) return;
|
||||
useHotKey('requestSwitcher.prev', () => {
|
||||
if (!dropdownRef.current?.isOpen) dropdownRef.current?.open(1);
|
||||
dropdownRef.current?.next?.();
|
||||
});
|
||||
|
||||
if (!dropdownRef.current?.isOpen) {
|
||||
dropdownRef.current?.open(e.shiftKey ? -1 : 1);
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.shiftKey) dropdownRef.current?.prev?.();
|
||||
else dropdownRef.current?.next?.();
|
||||
},
|
||||
undefined,
|
||||
[recentRequestIds.length],
|
||||
);
|
||||
useHotKey('requestSwitcher.next', () => {
|
||||
if (!dropdownRef.current?.isOpen) dropdownRef.current?.open(-1);
|
||||
dropdownRef.current?.prev?.();
|
||||
});
|
||||
|
||||
const items = useMemo<DropdownItem[]>(() => {
|
||||
if (activeWorkspaceId === null) return [];
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
import { useRef } from 'react';
|
||||
import { useDeleteRequest } from '../hooks/useDeleteRequest';
|
||||
import { useDuplicateRequest } from '../hooks/useDuplicateRequest';
|
||||
import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
|
||||
import type { DropdownProps, DropdownRef } from './core/Dropdown';
|
||||
import { Dropdown } from './core/Dropdown';
|
||||
import { HotKey } from './core/HotKey';
|
||||
import { Icon } from './core/Icon';
|
||||
|
||||
interface Props {
|
||||
requestId: string | null;
|
||||
children: DropdownProps['children'];
|
||||
}
|
||||
|
||||
export function RequestActionsDropdown({ requestId, children }: Props) {
|
||||
const deleteRequest = useDeleteRequest(requestId);
|
||||
const duplicateRequest = useDuplicateRequest({ id: requestId, navigateAfter: true });
|
||||
const dropdownRef = useRef<DropdownRef>(null);
|
||||
|
||||
useListenToTauriEvent('toggle_settings', () => {
|
||||
dropdownRef.current?.toggle();
|
||||
});
|
||||
|
||||
// TODO: Put this somewhere better
|
||||
useListenToTauriEvent('duplicate_request', () => {
|
||||
duplicateRequest.mutate();
|
||||
});
|
||||
|
||||
if (requestId == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
ref={dropdownRef}
|
||||
items={[
|
||||
{
|
||||
key: 'duplicate',
|
||||
label: 'Duplicate',
|
||||
onSelect: duplicateRequest.mutate,
|
||||
leftSlot: <Icon icon="copy" />,
|
||||
rightSlot: <HotKey modifier="Meta" keyName="D" />,
|
||||
},
|
||||
{
|
||||
key: 'delete',
|
||||
label: 'Delete',
|
||||
onSelect: deleteRequest.mutate,
|
||||
variant: 'danger',
|
||||
leftSlot: <Icon icon="trash" />,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{children}
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +1,8 @@
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { appWindow } from '@tauri-apps/api/window';
|
||||
import classNames from 'classnames';
|
||||
import type { CSSProperties } from 'react';
|
||||
import { memo, useCallback, useMemo, useState } from 'react';
|
||||
import { createGlobalState } from 'react-use';
|
||||
import { useActiveEnvironmentId } from '../hooks/useActiveEnvironmentId';
|
||||
import { useActiveRequest } from '../hooks/useActiveRequest';
|
||||
import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
|
||||
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
|
||||
import { useUpdateRequest } from '../hooks/useUpdateRequest';
|
||||
import { tryFormatJson } from '../lib/formatters';
|
||||
@@ -47,7 +43,6 @@ const useActiveTab = createGlobalState<string>('body');
|
||||
export const RequestPane = memo(function RequestPane({ style, fullHeight, className }: Props) {
|
||||
const activeRequest = useActiveRequest();
|
||||
const activeRequestId = activeRequest?.id ?? null;
|
||||
const activeEnvironmentId = useActiveEnvironmentId();
|
||||
const updateRequest = useUpdateRequest(activeRequestId);
|
||||
const [activeTab, setActiveTab] = useActiveTab();
|
||||
const [forceUpdateHeaderEditorKey, setForceUpdateHeaderEditorKey] = useState<number>(0);
|
||||
@@ -183,18 +178,6 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN
|
||||
[updateRequest],
|
||||
);
|
||||
|
||||
useListenToTauriEvent(
|
||||
'send_request',
|
||||
async ({ windowLabel }) => {
|
||||
if (windowLabel !== appWindow.label) return;
|
||||
await invoke('send_request', {
|
||||
requestId: activeRequestId,
|
||||
environmentId: activeEnvironmentId,
|
||||
});
|
||||
},
|
||||
[activeRequestId, activeEnvironmentId],
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={style}
|
||||
@@ -202,7 +185,12 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN
|
||||
>
|
||||
{activeRequest && (
|
||||
<>
|
||||
<UrlBar id={activeRequest.id} url={activeRequest.url} method={activeRequest.method} />
|
||||
<UrlBar
|
||||
key={activeRequest.id} // Force-reset the url bar when the active request changes
|
||||
id={activeRequest.id}
|
||||
url={activeRequest.url}
|
||||
method={activeRequest.method}
|
||||
/>
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
label="Request"
|
||||
|
||||
@@ -3,8 +3,10 @@ import classNames from 'classnames';
|
||||
import type { CSSProperties, MouseEvent as ReactMouseEvent } from 'react';
|
||||
import React, { memo, useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { useLocalStorage } from 'react-use';
|
||||
import { useActiveRequest } from '../hooks/useActiveRequest';
|
||||
import { useActiveWorkspaceId } from '../hooks/useActiveWorkspaceId';
|
||||
import { clamp } from '../lib/clamp';
|
||||
import { HotKeyList } from './core/HotKeyList';
|
||||
import { RequestPane } from './RequestPane';
|
||||
import { ResizeHandle } from './ResizeHandle';
|
||||
import { ResponsePane } from './ResponsePane';
|
||||
@@ -24,6 +26,7 @@ const STACK_VERTICAL_WIDTH = 600;
|
||||
|
||||
export const RequestResponse = memo(function RequestResponse({ style }: Props) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const activeRequest = useActiveRequest();
|
||||
const [vertical, setVertical] = useState<boolean>(false);
|
||||
const [widthRaw, setWidth] = useLocalStorage<number>(`body_width::${useActiveWorkspaceId()}`);
|
||||
const [heightRaw, setHeight] = useLocalStorage<number>(`body_height::${useActiveWorkspaceId()}`);
|
||||
@@ -35,7 +38,8 @@ export const RequestResponse = memo(function RequestResponse({ style }: Props) {
|
||||
);
|
||||
|
||||
useResizeObserver(containerRef, ({ contentRect }) => {
|
||||
setVertical(contentRect.width < STACK_VERTICAL_WIDTH);
|
||||
const doIt = contentRect.width < STACK_VERTICAL_WIDTH;
|
||||
setVertical(doIt);
|
||||
});
|
||||
|
||||
const styles = useMemo<CSSProperties>(
|
||||
@@ -114,6 +118,10 @@ export const RequestResponse = memo(function RequestResponse({ style }: Props) {
|
||||
[width, height, vertical, setHeight, setWidth],
|
||||
);
|
||||
|
||||
if (activeRequest === null) {
|
||||
return <HotKeyList hotkeys={['request.create', 'sidebar.toggle']} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="grid gap-1.5 w-full h-full p-3" style={styles}>
|
||||
<RequestPane style={rqst} fullHeight={!vertical} />
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import classNames from 'classnames';
|
||||
import type { CSSProperties } from 'react';
|
||||
import { useCallback, memo, useEffect, useMemo, useState } from 'react';
|
||||
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { createGlobalState } from 'react-use';
|
||||
import { useActiveRequestId } from '../hooks/useActiveRequestId';
|
||||
import { useActiveRequest } from '../hooks/useActiveRequest';
|
||||
import { useLatestResponse } from '../hooks/useLatestResponse';
|
||||
import { useResponseContentType } from '../hooks/useResponseContentType';
|
||||
import { useResponses } from '../hooks/useResponses';
|
||||
@@ -12,18 +12,19 @@ import { isResponseLoading } from '../lib/models';
|
||||
import { Banner } from './core/Banner';
|
||||
import { CountBadge } from './core/CountBadge';
|
||||
import { DurationTag } from './core/DurationTag';
|
||||
import { HotKeyList } from './core/HotKeyList';
|
||||
import { SizeTag } from './core/SizeTag';
|
||||
import { HStack } from './core/Stacks';
|
||||
import { StatusTag } from './core/StatusTag';
|
||||
import type { TabItem } from './core/Tabs/Tabs';
|
||||
import { TabContent, Tabs } from './core/Tabs/Tabs';
|
||||
import { EmptyStateText } from './EmptyStateText';
|
||||
import { RecentResponsesDropdown } from './RecentResponsesDropdown';
|
||||
import { ResponseHeaders } from './ResponseHeaders';
|
||||
import { CsvViewer } from './responseViewers/CsvViewer';
|
||||
import { ImageViewer } from './responseViewers/ImageViewer';
|
||||
import { TextViewer } from './responseViewers/TextViewer';
|
||||
import { WebPageViewer } from './responseViewers/WebPageViewer';
|
||||
import { RecentResponsesDropdown } from './RecentResponsesDropdown';
|
||||
|
||||
interface Props {
|
||||
style?: CSSProperties;
|
||||
@@ -34,9 +35,9 @@ const useActiveTab = createGlobalState<string>('body');
|
||||
|
||||
export const ResponsePane = memo(function ResponsePane({ style, className }: Props) {
|
||||
const [pinnedResponseId, setPinnedResponseId] = useState<string | null>(null);
|
||||
const activeRequestId = useActiveRequestId();
|
||||
const latestResponse = useLatestResponse(activeRequestId);
|
||||
const responses = useResponses(activeRequestId);
|
||||
const activeRequest = useActiveRequest();
|
||||
const latestResponse = useLatestResponse(activeRequest?.id ?? null);
|
||||
const responses = useResponses(activeRequest?.id ?? null);
|
||||
const activeResponse: HttpResponse | null = pinnedResponseId
|
||||
? responses.find((r) => r.id === pinnedResponseId) ?? null
|
||||
: latestResponse ?? null;
|
||||
@@ -48,11 +49,14 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro
|
||||
|
||||
const contentType = useResponseContentType(activeResponse);
|
||||
|
||||
const handlePinnedResponse = useCallback((r: HttpResponse) => {
|
||||
setPinnedResponseId(r.id);
|
||||
}, [setPinnedResponseId])
|
||||
const handlePinnedResponse = useCallback(
|
||||
(r: HttpResponse) => {
|
||||
setPinnedResponseId(r.id);
|
||||
},
|
||||
[setPinnedResponseId],
|
||||
);
|
||||
|
||||
const tabs: TabItem[] = useMemo(
|
||||
const tabs = useMemo<TabItem[]>(
|
||||
() => [
|
||||
{
|
||||
value: 'body',
|
||||
@@ -62,7 +66,7 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro
|
||||
onChange: setViewMode,
|
||||
items: [
|
||||
{ label: 'Pretty', value: 'pretty' },
|
||||
{ label: 'Raw', value: 'raw' },
|
||||
...(contentType?.startsWith('image') ? [] : [{ label: 'Raw', value: 'raw' }]),
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -78,9 +82,13 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro
|
||||
value: 'headers',
|
||||
},
|
||||
],
|
||||
[activeResponse?.headers, setViewMode, viewMode],
|
||||
[activeResponse?.headers, contentType, setViewMode, viewMode],
|
||||
);
|
||||
|
||||
if (activeRequest === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={style}
|
||||
@@ -92,6 +100,14 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro
|
||||
)}
|
||||
>
|
||||
{activeResponse?.error && <Banner className="m-2">{activeResponse.error}</Banner>}
|
||||
{!activeResponse && (
|
||||
<>
|
||||
<span />
|
||||
<HotKeyList
|
||||
hotkeys={['request.send', 'request.create', 'sidebar.toggle', 'urlBar.focus']}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{activeResponse && !activeResponse.error && !isResponseLoading(activeResponse) && (
|
||||
<>
|
||||
<HStack
|
||||
@@ -145,10 +161,14 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro
|
||||
<TabContent value="body">
|
||||
{!activeResponse.contentLength ? (
|
||||
<EmptyStateText>Empty Body</EmptyStateText>
|
||||
) : viewMode === 'pretty' && contentType?.includes('html') ? (
|
||||
<WebPageViewer response={activeResponse} />
|
||||
) : contentType?.startsWith('image') ? (
|
||||
<ImageViewer className="pb-2" response={activeResponse} />
|
||||
) : activeResponse.contentLength > 2 * 1000 * 1000 ? (
|
||||
<div className="text-sm italic text-gray-500">
|
||||
Cannot preview text responses larger than 2MB
|
||||
</div>
|
||||
) : viewMode === 'pretty' && contentType?.includes('html') ? (
|
||||
<WebPageViewer response={activeResponse} />
|
||||
) : contentType?.match(/csv|tab-separated/) ? (
|
||||
<CsvViewer className="pb-2" response={activeResponse} />
|
||||
) : (
|
||||
|
||||
83
src-web/components/SettingsDialog.tsx
Normal file
83
src-web/components/SettingsDialog.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import classNames from 'classnames';
|
||||
import { useSettings } from '../hooks/useSettings';
|
||||
import { useTheme } from '../hooks/useTheme';
|
||||
import { useUpdateSettings } from '../hooks/useUpdateSettings';
|
||||
import type { Appearance } from '../lib/theme/window';
|
||||
import { Checkbox } from './core/Checkbox';
|
||||
import { Input } from './core/Input';
|
||||
import { VStack } from './core/Stacks';
|
||||
|
||||
export const SettingsDialog = () => {
|
||||
const { appearance, setAppearance } = useTheme();
|
||||
const settings = useSettings();
|
||||
const updateSettings = useUpdateSettings();
|
||||
|
||||
if (settings == null) {
|
||||
return null;
|
||||
}
|
||||
console.log('SETTINGS', settings);
|
||||
|
||||
return (
|
||||
<VStack space={2}>
|
||||
<div className="w-full gap-2 grid grid-cols-[auto_1fr] gap-x-6 auto-rows-[2rem] items-center">
|
||||
<Checkbox
|
||||
className="col-span-full"
|
||||
checked={settings.validateCertificates}
|
||||
title="Validate TLS Certificates"
|
||||
onChange={(validateCertificates) =>
|
||||
updateSettings.mutateAsync({ ...settings, validateCertificates })
|
||||
}
|
||||
/>
|
||||
|
||||
<Checkbox
|
||||
className="col-span-full"
|
||||
checked={settings.followRedirects}
|
||||
title="Follow Redirects"
|
||||
onChange={(followRedirects) =>
|
||||
updateSettings.mutateAsync({ ...settings, followRedirects })
|
||||
}
|
||||
/>
|
||||
|
||||
<div>Request Timeout (ms)</div>
|
||||
<div>
|
||||
<Input
|
||||
size="sm"
|
||||
name="requestTimeout"
|
||||
label="Request Timeout (ms)"
|
||||
containerClassName="col-span-2"
|
||||
hideLabel
|
||||
defaultValue={`${settings.requestTimeout}`}
|
||||
validate={(value) => parseInt(value) >= 0}
|
||||
onChange={(v) =>
|
||||
updateSettings.mutateAsync({ ...settings, requestTimeout: parseInt(v) || 0 })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>Appearance</div>
|
||||
<select
|
||||
value={settings.appearance}
|
||||
style={selectBackgroundStyles}
|
||||
onChange={(e) => updateSettings.mutateAsync({ ...settings, appearance: e.target.value })}
|
||||
className={classNames(
|
||||
'border w-full px-2 outline-none bg-transparent',
|
||||
'border-highlight focus:border-focus',
|
||||
'h-sm',
|
||||
)}
|
||||
>
|
||||
<option value="system">Match System</option>
|
||||
<option value="light">Light</option>
|
||||
<option value="dark">Dark</option>
|
||||
</select>
|
||||
</div>
|
||||
{/*<Checkbox checked={appearance === 'dark'} title="Dark Mode" onChange={toggleAppearance} />*/}
|
||||
</VStack>
|
||||
);
|
||||
};
|
||||
|
||||
const selectBackgroundStyles = {
|
||||
backgroundImage: `url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e")`,
|
||||
backgroundPosition: 'right 0.5rem center',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundSize: '1.5em 1.5em',
|
||||
};
|
||||
115
src-web/components/SettingsDropdown.tsx
Normal file
115
src-web/components/SettingsDropdown.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import { invoke, shell } from '@tauri-apps/api';
|
||||
import { useRef } from 'react';
|
||||
import { useAppVersion } from '../hooks/useAppVersion';
|
||||
import { useExportData } from '../hooks/useExportData';
|
||||
import { useImportData } from '../hooks/useImportData';
|
||||
import { useUpdateMode } from '../hooks/useUpdateMode';
|
||||
import { Button } from './core/Button';
|
||||
import type { DropdownRef } from './core/Dropdown';
|
||||
import { Dropdown } from './core/Dropdown';
|
||||
import { Icon } from './core/Icon';
|
||||
import { IconButton } from './core/IconButton';
|
||||
import { VStack } from './core/Stacks';
|
||||
import { useDialog } from './DialogContext';
|
||||
import { KeyboardShortcutsDialog } from './KeyboardShortcutsDialog';
|
||||
import { SettingsDialog } from './SettingsDialog';
|
||||
|
||||
export function SettingsDropdown() {
|
||||
const importData = useImportData();
|
||||
const exportData = useExportData();
|
||||
const appVersion = useAppVersion();
|
||||
const [updateMode, setUpdateMode] = useUpdateMode();
|
||||
const dropdownRef = useRef<DropdownRef>(null);
|
||||
const dialog = useDialog();
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
ref={dropdownRef}
|
||||
items={[
|
||||
{
|
||||
key: 'import-data',
|
||||
label: 'Import',
|
||||
leftSlot: <Icon icon="download" />,
|
||||
onSelect: () => {
|
||||
dialog.show({
|
||||
title: 'Import Data',
|
||||
size: 'sm',
|
||||
render: ({ hide }) => {
|
||||
return (
|
||||
<VStack space={3}>
|
||||
<p>Insomnia or Postman Collection v2/v2.1 formats are supported</p>
|
||||
<Button
|
||||
size="sm"
|
||||
color="primary"
|
||||
onClick={async () => {
|
||||
await importData.mutateAsync();
|
||||
hide();
|
||||
}}
|
||||
>
|
||||
Select File
|
||||
</Button>
|
||||
</VStack>
|
||||
);
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'export-data',
|
||||
label: 'Export',
|
||||
leftSlot: <Icon icon="upload" />,
|
||||
onSelect: () => exportData.mutate(),
|
||||
},
|
||||
{
|
||||
key: 'hotkeys',
|
||||
label: 'Keyboard shortcuts',
|
||||
hotkeyAction: 'hotkeys.showHelp',
|
||||
leftSlot: <Icon icon="keyboard" />,
|
||||
onSelect: () => {
|
||||
dialog.show({
|
||||
id: 'hotkey-help',
|
||||
title: 'Keyboard Shortcuts',
|
||||
size: 'sm',
|
||||
render: () => <KeyboardShortcutsDialog />,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'settings',
|
||||
label: 'Settings',
|
||||
hotkeyAction: 'settings.show',
|
||||
leftSlot: <Icon icon="gear" />,
|
||||
onSelect: () => {
|
||||
dialog.show({
|
||||
id: 'settings',
|
||||
size: 'md',
|
||||
title: 'Settings',
|
||||
render: () => <SettingsDialog />,
|
||||
});
|
||||
},
|
||||
},
|
||||
{ type: 'separator', label: `Yaak v${appVersion.data}` },
|
||||
{
|
||||
key: 'update-mode',
|
||||
label: updateMode === 'stable' ? 'Enable Beta' : 'Disable Beta',
|
||||
onSelect: () => setUpdateMode(updateMode === 'stable' ? 'beta' : 'stable'),
|
||||
leftSlot: <Icon icon="rocket" />,
|
||||
},
|
||||
{
|
||||
key: 'update-check',
|
||||
label: 'Check for Updates',
|
||||
onSelect: () => invoke('check_for_updates'),
|
||||
leftSlot: <Icon icon="update" />,
|
||||
},
|
||||
{
|
||||
key: 'feedback',
|
||||
label: 'Feedback',
|
||||
onSelect: () => shell.open('https://yaak.canny.io'),
|
||||
leftSlot: <Icon icon="chat" />,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<IconButton size="sm" title="Main Menu" icon="hamburger" className="pointer-events-auto" />
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import type { XYCoord } from 'react-dnd';
|
||||
import { useDrag, useDrop } from 'react-dnd';
|
||||
import { useKey, useKeyPressEvent } from 'react-use';
|
||||
|
||||
import { showMenu } from 'tauri-plugin-context-menu';
|
||||
import { useActiveEnvironmentId } from '../hooks/useActiveEnvironmentId';
|
||||
import { useActiveRequestId } from '../hooks/useActiveRequestId';
|
||||
import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
|
||||
@@ -15,14 +14,14 @@ import { useCreateRequest } from '../hooks/useCreateRequest';
|
||||
import { useDeleteAnyRequest } from '../hooks/useDeleteAnyRequest';
|
||||
import { useDeleteFolder } from '../hooks/useDeleteFolder';
|
||||
import { useDeleteRequest } from '../hooks/useDeleteRequest';
|
||||
import { useDuplicateRequest } from '../hooks/useDuplicateRequest';
|
||||
import { useFolders } from '../hooks/useFolders';
|
||||
import { useHotKey } from '../hooks/useHotKey';
|
||||
import { useKeyValue } from '../hooks/useKeyValue';
|
||||
import { useLatestResponse } from '../hooks/useLatestResponse';
|
||||
import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
|
||||
import { usePrompt } from '../hooks/usePrompt';
|
||||
import { useRequests } from '../hooks/useRequests';
|
||||
import { useSendManyRequests } from '../hooks/useSendFolder';
|
||||
import { useSendRequest } from '../hooks/useSendRequest';
|
||||
import { useSidebarHidden } from '../hooks/useSidebarHidden';
|
||||
import { useUpdateAnyFolder } from '../hooks/useUpdateAnyFolder';
|
||||
import { useUpdateAnyRequest } from '../hooks/useUpdateAnyRequest';
|
||||
@@ -31,9 +30,8 @@ import { fallbackRequestName } from '../lib/fallbackRequestName';
|
||||
import { NAMESPACE_NO_SYNC } from '../lib/keyValueStore';
|
||||
import type { Folder, HttpRequest, Workspace } from '../lib/models';
|
||||
import { isResponseLoading } from '../lib/models';
|
||||
import { Dropdown } from './core/Dropdown';
|
||||
import { ContextMenu } from './core/Dropdown';
|
||||
import { Icon } from './core/Icon';
|
||||
import { IconButton } from './core/IconButton';
|
||||
import { InlineCode } from './core/InlineCode';
|
||||
import { VStack } from './core/Stacks';
|
||||
import { StatusTag } from './core/StatusTag';
|
||||
@@ -55,7 +53,6 @@ interface TreeNode {
|
||||
|
||||
export function Sidebar({ className }: Props) {
|
||||
const { hidden } = useSidebarHidden();
|
||||
const createRequest = useCreateRequest();
|
||||
const sidebarRef = useRef<HTMLLIElement>(null);
|
||||
const activeRequestId = useActiveRequestId();
|
||||
const activeEnvironmentId = useActiveEnvironmentId();
|
||||
@@ -86,10 +83,18 @@ export function Sidebar({ className }: Props) {
|
||||
const { tree, treeParentMap, selectableRequests } = useMemo<{
|
||||
tree: TreeNode | null;
|
||||
treeParentMap: Record<string, TreeNode>;
|
||||
selectableRequests: { id: string; index: number; tree: TreeNode }[];
|
||||
selectableRequests: {
|
||||
id: string;
|
||||
index: number;
|
||||
tree: TreeNode;
|
||||
}[];
|
||||
}>(() => {
|
||||
const treeParentMap: Record<string, TreeNode> = {};
|
||||
const selectableRequests: { id: string; index: number; tree: TreeNode }[] = [];
|
||||
const selectableRequests: {
|
||||
id: string;
|
||||
index: number;
|
||||
tree: TreeNode;
|
||||
}[] = [];
|
||||
if (activeWorkspace == null) {
|
||||
return { tree: null, treeParentMap, selectableRequests };
|
||||
}
|
||||
@@ -119,11 +124,16 @@ export function Sidebar({ className }: Props) {
|
||||
return { tree, treeParentMap, selectableRequests };
|
||||
}, [activeWorkspace, requests, folders]);
|
||||
|
||||
// TODO: Move these listeners to a central place
|
||||
useListenToTauriEvent('new_request', async () => createRequest.mutate({}));
|
||||
|
||||
const focusActiveRequest = useCallback(
|
||||
(args: { forced?: { id: string; tree: TreeNode }; noFocusSidebar?: boolean } = {}) => {
|
||||
(
|
||||
args: {
|
||||
forced?: {
|
||||
id: string;
|
||||
tree: TreeNode;
|
||||
};
|
||||
noFocusSidebar?: boolean;
|
||||
} = {},
|
||||
) => {
|
||||
const { forced, noFocusSidebar } = args;
|
||||
const tree = forced?.tree ?? treeParentMap[activeRequestId ?? 'n/a'] ?? null;
|
||||
const children = tree?.children ?? [];
|
||||
@@ -196,19 +206,15 @@ export function Sidebar({ className }: Props) {
|
||||
useKeyPressEvent('Backspace', handleDeleteKey);
|
||||
useKeyPressEvent('Delete', handleDeleteKey);
|
||||
|
||||
useListenToTauriEvent(
|
||||
'focus_sidebar',
|
||||
() => {
|
||||
if (hidden || hasFocus) return;
|
||||
// Select 0 index on focus if none selected
|
||||
focusActiveRequest(
|
||||
selectedTree != null && selectedId != null
|
||||
? { forced: { id: selectedId, tree: selectedTree } }
|
||||
: undefined,
|
||||
);
|
||||
},
|
||||
[focusActiveRequest, hidden, activeRequestId],
|
||||
);
|
||||
useHotKey('sidebar.focus', () => {
|
||||
if (hidden || hasFocus) return;
|
||||
// Select 0 index on focus if none selected
|
||||
focusActiveRequest(
|
||||
selectedTree != null && selectedId != null
|
||||
? { forced: { id: selectedId, tree: selectedTree } }
|
||||
: undefined,
|
||||
);
|
||||
});
|
||||
|
||||
useKeyPressEvent('Enter', (e) => {
|
||||
if (!hasFocus) return;
|
||||
@@ -310,8 +316,9 @@ export function Sidebar({ className }: Props) {
|
||||
newChildren.splice(hoveredIndex - 1, 0, child);
|
||||
}
|
||||
|
||||
const prev = newChildren[hoveredIndex - 1]?.item;
|
||||
const next = newChildren[hoveredIndex + 1]?.item;
|
||||
const insertedIndex = newChildren.findIndex((c) => c.item === child.item);
|
||||
const prev = newChildren[insertedIndex - 1]?.item;
|
||||
const next = newChildren[insertedIndex + 1]?.item;
|
||||
const beforePriority = prev == null || prev.model === 'workspace' ? 0 : prev.sortPriority;
|
||||
const afterPriority = next == null || next.model === 'workspace' ? 0 : next.sortPriority;
|
||||
|
||||
@@ -512,9 +519,9 @@ const SidebarItem = forwardRef(function SidebarItem(
|
||||
const createRequest = useCreateRequest();
|
||||
const createFolder = useCreateFolder();
|
||||
const deleteFolder = useDeleteFolder(itemId);
|
||||
const sendRequest = useSendRequest(itemId);
|
||||
const sendManyRequests = useSendManyRequests();
|
||||
const deleteRequest = useDeleteRequest(itemId);
|
||||
const duplicateRequest = useDuplicateRequest({ id: itemId, navigateAfter: true });
|
||||
const sendManyRequests = useSendManyRequests();
|
||||
const latestResponse = useLatestResponse(itemId);
|
||||
const updateRequest = useUpdateRequest(itemId);
|
||||
const updateAnyFolder = useUpdateAnyFolder();
|
||||
@@ -565,105 +572,93 @@ const SidebarItem = forwardRef(function SidebarItem(
|
||||
[handleSubmitNameEdit],
|
||||
);
|
||||
|
||||
const handleContextMenu = useCallback(
|
||||
(e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
showMenu({
|
||||
pos: { x: e.clientX, y: e.clientY },
|
||||
items:
|
||||
itemModel === 'http_request'
|
||||
? [
|
||||
{
|
||||
label: 'Send Request',
|
||||
event: () => sendRequest.mutate(),
|
||||
},
|
||||
{
|
||||
label: 'Delete Request',
|
||||
event: () => deleteRequest.mutate(),
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
label: 'Send All',
|
||||
event: () => sendManyRequests.mutate(child.children.map((c) => c.item.id)),
|
||||
},
|
||||
{
|
||||
label: 'Delete Folder',
|
||||
event: () => deleteFolder.mutate(),
|
||||
},
|
||||
],
|
||||
})
|
||||
.then((r) => console.log(r))
|
||||
.catch((e) => console.log(e));
|
||||
},
|
||||
[itemModel, sendRequest, deleteRequest, sendManyRequests, child.children, deleteFolder],
|
||||
);
|
||||
|
||||
const handleSelect = useCallback(() => onSelect(itemId), [onSelect, itemId]);
|
||||
const [showContextMenu, setShowContextMenu] = useState<{
|
||||
x: number;
|
||||
y: number;
|
||||
} | null>(null);
|
||||
|
||||
const handleContextMenu = useCallback((e: React.MouseEvent<HTMLElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setShowContextMenu({ x: e.clientX, y: e.clientY });
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<li ref={ref}>
|
||||
<div className={classNames(className, 'block relative group/item px-2 pb-0.5')}>
|
||||
{itemModel === 'folder' && (
|
||||
<Dropdown
|
||||
items={[
|
||||
{
|
||||
key: 'sendAll',
|
||||
label: 'Send All',
|
||||
leftSlot: <Icon icon="paperPlane" />,
|
||||
onSelect: () => sendManyRequests.mutate(child.children.map((c) => c.item.id)),
|
||||
},
|
||||
{ type: 'separator', label: itemName },
|
||||
{
|
||||
key: 'rename',
|
||||
label: 'Rename',
|
||||
leftSlot: <Icon icon="pencil" />,
|
||||
onSelect: async () => {
|
||||
const name = await prompt({
|
||||
title: 'Rename Folder',
|
||||
description: (
|
||||
<>
|
||||
Enter a new name for <InlineCode>{itemName}</InlineCode>
|
||||
</>
|
||||
),
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
defaultValue: itemName,
|
||||
});
|
||||
updateAnyFolder.mutate({ id: itemId, update: (f) => ({ ...f, name }) });
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'deleteFolder',
|
||||
label: 'Delete',
|
||||
variant: 'danger',
|
||||
leftSlot: <Icon icon="trash" />,
|
||||
onSelect: () => deleteFolder.mutate(),
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
key: 'createRequest',
|
||||
label: 'New Request',
|
||||
leftSlot: <Icon icon="plus" />,
|
||||
onSelect: () => createRequest.mutate({ folderId: itemId, sortPriority: -1 }),
|
||||
},
|
||||
{
|
||||
key: 'createFolder',
|
||||
label: 'New Folder',
|
||||
leftSlot: <Icon icon="plus" />,
|
||||
onSelect: () => createFolder.mutate({ folderId: itemId, sortPriority: -1 }),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<IconButton
|
||||
title="Folder options"
|
||||
size="xs"
|
||||
icon="dotsV"
|
||||
className="ml-auto !bg-transparent absolute right-2 opacity-0 group-hover/item:opacity-70 transition-opacity"
|
||||
/>
|
||||
</Dropdown>
|
||||
)}
|
||||
<ContextMenu
|
||||
show={showContextMenu}
|
||||
items={
|
||||
itemModel === 'folder'
|
||||
? [
|
||||
{
|
||||
key: 'sendAll',
|
||||
label: 'Send All',
|
||||
leftSlot: <Icon icon="paperPlane" />,
|
||||
onSelect: () => sendManyRequests.mutate(child.children.map((c) => c.item.id)),
|
||||
},
|
||||
{ type: 'separator', label: itemName },
|
||||
{
|
||||
key: 'rename',
|
||||
label: 'Rename',
|
||||
leftSlot: <Icon icon="pencil" />,
|
||||
onSelect: async () => {
|
||||
const name = await prompt({
|
||||
title: 'Rename Folder',
|
||||
description: (
|
||||
<>
|
||||
Enter a new name for <InlineCode>{itemName}</InlineCode>
|
||||
</>
|
||||
),
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
defaultValue: itemName,
|
||||
});
|
||||
updateAnyFolder.mutate({ id: itemId, update: (f) => ({ ...f, name }) });
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'deleteFolder',
|
||||
label: 'Delete',
|
||||
variant: 'danger',
|
||||
leftSlot: <Icon icon="trash" />,
|
||||
onSelect: () => deleteFolder.mutate(),
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
key: 'createRequest',
|
||||
label: 'New Request',
|
||||
hotkeyAction: 'request.create',
|
||||
leftSlot: <Icon icon="plus" />,
|
||||
onSelect: () => createRequest.mutate({ folderId: itemId, sortPriority: -1 }),
|
||||
},
|
||||
{
|
||||
key: 'createFolder',
|
||||
label: 'New Folder',
|
||||
leftSlot: <Icon icon="plus" />,
|
||||
onSelect: () => createFolder.mutate({ folderId: itemId, sortPriority: -1 }),
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
key: 'duplicateRequest',
|
||||
label: 'Duplicate',
|
||||
hotkeyAction: 'request.duplicate',
|
||||
leftSlot: <Icon icon="copy" />,
|
||||
onSelect: () => duplicateRequest.mutate(),
|
||||
},
|
||||
{
|
||||
key: 'deleteRequest',
|
||||
variant: 'danger',
|
||||
label: 'Delete',
|
||||
leftSlot: <Icon icon="trash" />,
|
||||
onSelect: () => deleteRequest.mutate(),
|
||||
},
|
||||
]
|
||||
}
|
||||
onClose={() => setShowContextMenu(null)}
|
||||
/>
|
||||
<button
|
||||
// tabIndex={-1} // Will prevent drag-n-drop
|
||||
disabled={editing}
|
||||
@@ -758,7 +753,13 @@ function DraggableSidebarItem({
|
||||
[onMove],
|
||||
);
|
||||
|
||||
const [{ isDragging }, connectDrag] = useDrag<DragItem, unknown, { isDragging: boolean }>(
|
||||
const [{ isDragging }, connectDrag] = useDrag<
|
||||
DragItem,
|
||||
unknown,
|
||||
{
|
||||
isDragging: boolean;
|
||||
}
|
||||
>(
|
||||
() => ({
|
||||
type: ItemTypes.REQUEST,
|
||||
item: () => {
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useCreateFolder } from '../hooks/useCreateFolder';
|
||||
import { useCreateRequest } from '../hooks/useCreateRequest';
|
||||
import { useSidebarHidden } from '../hooks/useSidebarHidden';
|
||||
import { Dropdown } from './core/Dropdown';
|
||||
import { Icon } from './core/Icon';
|
||||
import { IconButton } from './core/IconButton';
|
||||
import { HStack } from './core/Stacks';
|
||||
|
||||
@@ -19,6 +18,7 @@ export const SidebarActions = memo(function SidebarActions() {
|
||||
className="pointer-events-auto"
|
||||
size="sm"
|
||||
title="Show sidebar"
|
||||
hotkeyAction="sidebar.toggle"
|
||||
icon={hidden ? 'leftPanelHidden' : 'leftPanelVisible'}
|
||||
/>
|
||||
<Dropdown
|
||||
@@ -26,13 +26,12 @@ export const SidebarActions = memo(function SidebarActions() {
|
||||
{
|
||||
key: 'create-request',
|
||||
label: 'New Request',
|
||||
leftSlot: <Icon icon="plus" />,
|
||||
hotkeyAction: 'request.create',
|
||||
onSelect: () => createRequest.mutate({}),
|
||||
},
|
||||
{
|
||||
key: 'create-folder',
|
||||
label: 'New Folder',
|
||||
leftSlot: <Icon icon="plus" />,
|
||||
onSelect: () => createFolder.mutate({}),
|
||||
},
|
||||
]}
|
||||
|
||||
@@ -2,8 +2,8 @@ import classNames from 'classnames';
|
||||
import type { EditorView } from 'codemirror';
|
||||
import type { FormEvent } from 'react';
|
||||
import { memo, useCallback, useRef, useState } from 'react';
|
||||
import { useHotKey } from '../hooks/useHotKey';
|
||||
import { useIsResponseLoading } from '../hooks/useIsResponseLoading';
|
||||
import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
|
||||
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
|
||||
import { useSendRequest } from '../hooks/useSendRequest';
|
||||
import { useUpdateRequest } from '../hooks/useUpdateRequest';
|
||||
@@ -40,7 +40,11 @@ export const UrlBar = memo(function UrlBar({ id: requestId, url, method, classNa
|
||||
[sendRequest],
|
||||
);
|
||||
|
||||
useListenToTauriEvent('focus_url', () => {
|
||||
useHotKey('urlBar.focus', () => {
|
||||
const head = inputRef.current?.state.doc.length ?? 0;
|
||||
inputRef.current?.dispatch({
|
||||
selection: { anchor: 0, head },
|
||||
});
|
||||
inputRef.current?.focus();
|
||||
});
|
||||
|
||||
@@ -79,6 +83,7 @@ export const UrlBar = memo(function UrlBar({ id: requestId, url, method, classNa
|
||||
className="!h-auto w-8 mr-0.5 my-0.5"
|
||||
icon={loading ? 'update' : 'paperPlane'}
|
||||
spin={loading}
|
||||
hotkeyAction="request.send"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -8,7 +8,6 @@ import type {
|
||||
} from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useWindowSize } from 'react-use';
|
||||
import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
|
||||
import { useOsInfo } from '../hooks/useOsInfo';
|
||||
import { useSidebarHidden } from '../hooks/useSidebarHidden';
|
||||
import { useSidebarWidth } from '../hooks/useSidebarWidth';
|
||||
@@ -30,7 +29,7 @@ const WINDOW_FLOATING_SIDEBAR_WIDTH = 600;
|
||||
|
||||
export default function Workspace() {
|
||||
const { setWidth, width, resetWidth } = useSidebarWidth();
|
||||
const { hide, show, hidden, toggle } = useSidebarHidden();
|
||||
const { hide, show, hidden } = useSidebarHidden();
|
||||
|
||||
const windowSize = useWindowSize();
|
||||
const [floating, setFloating] = useState<boolean>(false);
|
||||
@@ -39,14 +38,16 @@ export default function Workspace() {
|
||||
null,
|
||||
);
|
||||
|
||||
useListenToTauriEvent('toggle_sidebar', toggle);
|
||||
|
||||
// float/un-float sidebar on window resize
|
||||
useEffect(() => {
|
||||
const shouldHide = windowSize.width <= WINDOW_FLOATING_SIDEBAR_WIDTH;
|
||||
if (shouldHide) setFloating(true);
|
||||
else if (!shouldHide) setFloating(false);
|
||||
}, [windowSize.width]);
|
||||
if (shouldHide && !floating) {
|
||||
setFloating(true);
|
||||
hide();
|
||||
} else if (!shouldHide && floating) {
|
||||
setFloating(false);
|
||||
}
|
||||
}, [floating, hide, windowSize.width]);
|
||||
|
||||
const unsub = () => {
|
||||
if (moveState.current !== null) {
|
||||
@@ -176,8 +177,8 @@ function HeaderSize({ className, ...props }: HeaderSizeProps) {
|
||||
<div
|
||||
className={classNames(
|
||||
className,
|
||||
'h-md pt-[1px] flex items-center w-full pr-3 pl-20 border-b',
|
||||
platform?.osType === 'Darwin' && 'pl-20',
|
||||
'h-md pt-[1px] flex items-center w-full border-b',
|
||||
platform?.osType === 'Darwin' ? 'pl-20 pr-1' : 'pl-1',
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@@ -3,15 +3,10 @@ import classNames from 'classnames';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
|
||||
import { useAppRoutes } from '../hooks/useAppRoutes';
|
||||
import { useAppVersion } from '../hooks/useAppVersion';
|
||||
import { useCreateWorkspace } from '../hooks/useCreateWorkspace';
|
||||
import { useDeleteWorkspace } from '../hooks/useDeleteWorkspace';
|
||||
import { useExportData } from '../hooks/useExportData';
|
||||
import { useImportData } from '../hooks/useImportData';
|
||||
import { usePrompt } from '../hooks/usePrompt';
|
||||
import { getRecentEnvironments } from '../hooks/useRecentEnvironments';
|
||||
import { useTheme } from '../hooks/useTheme';
|
||||
import { useUpdateMode } from '../hooks/useUpdateMode';
|
||||
import { useUpdateWorkspace } from '../hooks/useUpdateWorkspace';
|
||||
import { useWorkspaces } from '../hooks/useWorkspaces';
|
||||
import type { ButtonProps } from './core/Button';
|
||||
@@ -32,17 +27,12 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
|
||||
const workspaces = useWorkspaces();
|
||||
const activeWorkspace = useActiveWorkspace();
|
||||
const activeWorkspaceId = activeWorkspace?.id ?? null;
|
||||
const createWorkspace = useCreateWorkspace({ navigateAfter: true });
|
||||
const updateWorkspace = useUpdateWorkspace(activeWorkspaceId);
|
||||
const deleteWorkspace = useDeleteWorkspace(activeWorkspace);
|
||||
const importData = useImportData();
|
||||
const exportData = useExportData();
|
||||
const { appearance, toggleAppearance } = useTheme();
|
||||
const createWorkspace = useCreateWorkspace({ navigateAfter: true });
|
||||
const dialog = useDialog();
|
||||
const prompt = usePrompt();
|
||||
const routes = useAppRoutes();
|
||||
const appVersion = useAppVersion();
|
||||
const [updateMode, setUpdateMode] = useUpdateMode();
|
||||
|
||||
const items: DropdownItem[] = useMemo(() => {
|
||||
const workspaceItems: DropdownItem[] = workspaces.map((w) => ({
|
||||
@@ -149,52 +139,14 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
|
||||
createWorkspace.mutate({ name });
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'import-data',
|
||||
label: 'Import Data',
|
||||
leftSlot: <Icon icon="download" />,
|
||||
onSelect: () => importData.mutate(),
|
||||
},
|
||||
{
|
||||
key: 'export-data',
|
||||
label: 'Export Data',
|
||||
leftSlot: <Icon icon="upload" />,
|
||||
onSelect: () => exportData.mutate(),
|
||||
},
|
||||
{ type: 'separator', label: `v${appVersion.data}` },
|
||||
{
|
||||
key: 'appearance',
|
||||
label: 'Toggle Theme',
|
||||
onSelect: toggleAppearance,
|
||||
leftSlot: <Icon icon={appearance === 'dark' ? 'sun' : 'moon'} />,
|
||||
},
|
||||
{
|
||||
key: 'update-mode',
|
||||
label: updateMode === 'stable' ? 'Enable Beta' : 'Disable Beta',
|
||||
onSelect: () => setUpdateMode(updateMode === 'stable' ? 'beta' : 'stable'),
|
||||
leftSlot: <Icon icon="camera" />,
|
||||
},
|
||||
{
|
||||
key: 'update-check',
|
||||
label: 'Check for Updates',
|
||||
onSelect: () => invoke('check_for_updates'),
|
||||
leftSlot: <Icon icon="update" />,
|
||||
},
|
||||
];
|
||||
}, [
|
||||
activeWorkspace?.name,
|
||||
activeWorkspaceId,
|
||||
appearance,
|
||||
createWorkspace,
|
||||
deleteWorkspace.mutate,
|
||||
dialog,
|
||||
exportData,
|
||||
importData,
|
||||
prompt,
|
||||
routes,
|
||||
setUpdateMode,
|
||||
toggleAppearance,
|
||||
updateMode,
|
||||
updateWorkspace,
|
||||
workspaces,
|
||||
]);
|
||||
|
||||
@@ -1,24 +1,23 @@
|
||||
import classNames from 'classnames';
|
||||
import React, { memo } from 'react';
|
||||
import { useActiveRequest } from '../hooks/useActiveRequest';
|
||||
import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
|
||||
import React, { memo, useState } from 'react';
|
||||
import { Icon } from './core/Icon';
|
||||
import { IconButton } from './core/IconButton';
|
||||
import { HStack } from './core/Stacks';
|
||||
import { EnvironmentActionsDropdown } from './EnvironmentActionsDropdown';
|
||||
import { RecentRequestsDropdown } from './RecentRequestsDropdown';
|
||||
import { RequestActionsDropdown } from './RequestActionsDropdown';
|
||||
import { SettingsDropdown } from './SettingsDropdown';
|
||||
import { SidebarActions } from './SidebarActions';
|
||||
import { WorkspaceActionsDropdown } from './WorkspaceActionsDropdown';
|
||||
import { useOsInfo } from '../hooks/useOsInfo';
|
||||
import { Button } from './core/Button';
|
||||
import { appWindow } from '@tauri-apps/api/window';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const WorkspaceHeader = memo(function WorkspaceHeader({ className }: Props) {
|
||||
const activeRequest = useActiveRequest();
|
||||
const activeWorkspace = useActiveWorkspace();
|
||||
|
||||
const osInfo = useOsInfo();
|
||||
const [maximized, setMaximized] = useState<boolean>(false);
|
||||
return (
|
||||
<HStack
|
||||
space={2}
|
||||
@@ -29,13 +28,7 @@ export const WorkspaceHeader = memo(function WorkspaceHeader({ className }: Prop
|
||||
<HStack space={0.5} className="flex-1 pointer-events-none" alignItems="center">
|
||||
<SidebarActions />
|
||||
<HStack alignItems="center">
|
||||
<WorkspaceActionsDropdown
|
||||
leftSlot={
|
||||
<div className="w-4 h-4 leading-4 rounded text-[0.8em] bg-[#1B88DE] bg-opacity-80 text-white mr-1">
|
||||
{activeWorkspace?.name[0]?.toUpperCase()}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<WorkspaceActionsDropdown />
|
||||
<Icon icon="chevronRight" className="text-gray-900 text-opacity-disabled" />
|
||||
<EnvironmentActionsDropdown className="w-auto pointer-events-auto" />
|
||||
</HStack>
|
||||
@@ -43,15 +36,51 @@ export const WorkspaceHeader = memo(function WorkspaceHeader({ className }: Prop
|
||||
<div className="pointer-events-none">
|
||||
<RecentRequestsDropdown />
|
||||
</div>
|
||||
<div className="flex-1 flex justify-end -mr-2 pointer-events-none">
|
||||
<RequestActionsDropdown requestId={activeRequest?.id ?? null}>
|
||||
<IconButton
|
||||
size="sm"
|
||||
title="Request Options"
|
||||
icon="gear"
|
||||
className="pointer-events-auto"
|
||||
/>
|
||||
</RequestActionsDropdown>
|
||||
<div className="flex-1 flex items-center h-full justify-end pointer-events-none">
|
||||
<SettingsDropdown />
|
||||
{osInfo?.osType !== 'Darwin' && (
|
||||
<HStack className="ml-4" space={1} alignItems="center">
|
||||
<Button className="!text-gray-600 rounded-none" onClick={() => appWindow.minimize()}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<path fill="currentColor" d="M14 8v1H3V8z" />
|
||||
</svg>
|
||||
</Button>
|
||||
<Button
|
||||
className="!text-gray-600 rounded-none"
|
||||
onClick={async () => {
|
||||
await appWindow.toggleMaximize();
|
||||
setMaximized(await appWindow.isMaximized());
|
||||
}}
|
||||
>
|
||||
{maximized ? (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<path fill="currentColor" d="M3 3v10h10V3zm9 9H4V4h8z" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<g fill="currentColor">
|
||||
<path d="M3 5v9h9V5zm8 8H4V6h7z" />
|
||||
<path fillRule="evenodd" d="M5 5h1V4h7v7h-1v1h2V3H5z" clipRule="evenodd" />
|
||||
</g>
|
||||
</svg>
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
color="custom"
|
||||
className="text-gray-600 rounded-none hocus:bg-red-200 hocus:text-gray-800"
|
||||
onClick={() => appWindow.close()}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<path
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
d="m7.116 8l-4.558 4.558l.884.884L8 8.884l4.558 4.558l.884-.884L8.884 8l4.558-4.558l-.884-.884L8 7.116L3.442 2.558l-.884.884z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</Button>
|
||||
</HStack>
|
||||
)}
|
||||
</div>
|
||||
</HStack>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import classNames from 'classnames';
|
||||
import type { HTMLAttributes, ReactNode } from 'react';
|
||||
import { forwardRef, memo, useMemo } from 'react';
|
||||
import { forwardRef, useImperativeHandle, useMemo, useRef } from 'react';
|
||||
import type { HotkeyAction } from '../../hooks/useHotKey';
|
||||
import { useFormattedHotkey, useHotKey } from '../../hooks/useHotKey';
|
||||
import { Icon } from './Icon';
|
||||
|
||||
const colorStyles = {
|
||||
@@ -26,10 +28,10 @@ export type ButtonProps = HTMLAttributes<HTMLButtonElement> & {
|
||||
title?: string;
|
||||
leftSlot?: ReactNode;
|
||||
rightSlot?: ReactNode;
|
||||
hotkeyAction?: HotkeyAction;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const _Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
|
||||
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
|
||||
{
|
||||
isLoading,
|
||||
className,
|
||||
@@ -43,10 +45,16 @@ const _Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
|
||||
leftSlot,
|
||||
rightSlot,
|
||||
disabled,
|
||||
hotkeyAction,
|
||||
title,
|
||||
onClick,
|
||||
...props
|
||||
}: ButtonProps,
|
||||
ref,
|
||||
) {
|
||||
const hotkeyTrigger = useFormattedHotkey(hotkeyAction ?? null)?.join('');
|
||||
const fullTitle = hotkeyTrigger ? `${title} ${hotkeyTrigger}` : title;
|
||||
|
||||
const classes = useMemo(
|
||||
() =>
|
||||
classNames(
|
||||
@@ -66,8 +74,26 @@ const _Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
|
||||
[className, disabled, color, justify, size],
|
||||
);
|
||||
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
useImperativeHandle<HTMLButtonElement | null, HTMLButtonElement | null>(
|
||||
ref,
|
||||
() => buttonRef.current,
|
||||
);
|
||||
|
||||
useHotKey(hotkeyAction ?? null, () => {
|
||||
buttonRef.current?.click();
|
||||
});
|
||||
|
||||
return (
|
||||
<button ref={ref} type={type} className={classes} disabled={disabled} {...props}>
|
||||
<button
|
||||
ref={buttonRef}
|
||||
type={type}
|
||||
className={classes}
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
title={fullTitle}
|
||||
{...props}
|
||||
>
|
||||
{isLoading ? (
|
||||
<Icon icon="update" size={size} className="animate-spin mr-1" />
|
||||
) : leftSlot ? (
|
||||
@@ -87,5 +113,3 @@ const _Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
|
||||
</button>
|
||||
);
|
||||
});
|
||||
|
||||
export const Button = memo(_Button);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import classNames from 'classnames';
|
||||
import { useCallback } from 'react';
|
||||
import { Icon } from './Icon';
|
||||
import { HStack } from './Stacks';
|
||||
|
||||
interface Props {
|
||||
checked: boolean;
|
||||
@@ -8,33 +8,47 @@ interface Props {
|
||||
onChange: (checked: boolean) => void;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
hideLabel?: boolean;
|
||||
}
|
||||
|
||||
export function Checkbox({ checked, onChange, className, disabled, title }: Props) {
|
||||
const handleClick = useCallback(() => {
|
||||
onChange(!checked);
|
||||
}, [onChange, checked]);
|
||||
|
||||
export function Checkbox({ checked, onChange, className, disabled, title, hideLabel }: Props) {
|
||||
return (
|
||||
<button
|
||||
role="checkbox"
|
||||
aria-checked={checked ? 'true' : 'false'}
|
||||
disabled={disabled}
|
||||
onClick={handleClick}
|
||||
title={title}
|
||||
className={classNames(
|
||||
className,
|
||||
'flex-shrink-0 w-4 h-4 border border-gray-200 rounded',
|
||||
'focus:border-focus',
|
||||
'disabled:opacity-disabled',
|
||||
checked && 'bg-gray-200/10',
|
||||
// Remove focus style
|
||||
'outline-none',
|
||||
)}
|
||||
<HStack
|
||||
as="label"
|
||||
space={2}
|
||||
alignItems="center"
|
||||
className={classNames(className, disabled && 'opacity-disabled')}
|
||||
>
|
||||
<div className="flex items-center justify-center">
|
||||
<Icon size="sm" icon={checked ? 'check' : 'empty'} />
|
||||
<div className="relative flex">
|
||||
<input
|
||||
aria-hidden
|
||||
className="appearance-none w-4 h-4 flex-shrink-0 border border-gray-200 rounded focus:border-focus outline-none ring-0"
|
||||
type="checkbox"
|
||||
disabled={disabled}
|
||||
onChange={() => onChange(!checked)}
|
||||
/>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<Icon size="sm" icon={checked ? 'check' : 'empty'} />
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
{/*<button*/}
|
||||
{/* role="checkbox"*/}
|
||||
{/* aria-checked={checked ? 'true' : 'false'}*/}
|
||||
{/* disabled={disabled}*/}
|
||||
{/* onClick={handleClick}*/}
|
||||
{/* title={title}*/}
|
||||
{/* className={classNames(*/}
|
||||
{/* className,*/}
|
||||
{/* 'flex-shrink-0 w-4 h-4 border border-gray-200 rounded',*/}
|
||||
{/* 'focus:border-focus',*/}
|
||||
{/* 'disabled:opacity-disabled',*/}
|
||||
{/* checked && 'bg-gray-200/10',*/}
|
||||
{/* // Remove focus style*/}
|
||||
{/* 'outline-none',*/}
|
||||
{/* )}*/}
|
||||
{/*>*/}
|
||||
{/*</button>*/}
|
||||
{!hideLabel && title}
|
||||
</HStack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,8 +20,11 @@ import React, {
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useKey, useKeyPressEvent, useWindowSize } from 'react-use';
|
||||
import type { HotkeyAction } from '../../hooks/useHotKey';
|
||||
import { useHotKey } from '../../hooks/useHotKey';
|
||||
import { Overlay } from '../Overlay';
|
||||
import { Button } from './Button';
|
||||
import { HotKey } from './HotKey';
|
||||
import { Separator } from './Separator';
|
||||
import { VStack } from './Stacks';
|
||||
|
||||
@@ -30,23 +33,25 @@ export type DropdownItemSeparator = {
|
||||
label?: string;
|
||||
};
|
||||
|
||||
export type DropdownItem =
|
||||
| {
|
||||
key: string;
|
||||
type?: 'default';
|
||||
label: ReactNode;
|
||||
variant?: 'danger';
|
||||
disabled?: boolean;
|
||||
hidden?: boolean;
|
||||
leftSlot?: ReactNode;
|
||||
rightSlot?: ReactNode;
|
||||
onSelect?: () => void;
|
||||
}
|
||||
| DropdownItemSeparator;
|
||||
export type DropdownItemDefault = {
|
||||
key: string;
|
||||
type?: 'default';
|
||||
label: ReactNode;
|
||||
hotkeyAction?: HotkeyAction;
|
||||
variant?: 'danger';
|
||||
disabled?: boolean;
|
||||
hidden?: boolean;
|
||||
leftSlot?: ReactNode;
|
||||
rightSlot?: ReactNode;
|
||||
onSelect?: () => void;
|
||||
};
|
||||
|
||||
export type DropdownItem = DropdownItemDefault | DropdownItemSeparator;
|
||||
|
||||
export interface DropdownProps {
|
||||
children: ReactElement<HTMLAttributes<HTMLButtonElement>>;
|
||||
items: DropdownItem[];
|
||||
openOnHotKeyAction?: HotkeyAction;
|
||||
}
|
||||
|
||||
export interface DropdownRef {
|
||||
@@ -60,20 +65,24 @@ export interface DropdownRef {
|
||||
}
|
||||
|
||||
export const Dropdown = forwardRef<DropdownRef, DropdownProps>(function Dropdown(
|
||||
{ children, items }: DropdownProps,
|
||||
{ children, items, openOnHotKeyAction }: DropdownProps,
|
||||
ref,
|
||||
) {
|
||||
const [open, setOpen] = useState<boolean>(false);
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
const [defaultSelectedIndex, setDefaultSelectedIndex] = useState<number>();
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
const menuRef = useRef<Omit<DropdownRef, 'open'>>(null);
|
||||
|
||||
useHotKey(openOnHotKeyAction ?? null, () => {
|
||||
setIsOpen(true);
|
||||
});
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
...menuRef.current,
|
||||
isOpen: open,
|
||||
isOpen: isOpen,
|
||||
toggle(activeIndex?: number) {
|
||||
if (!open) this.open(activeIndex);
|
||||
else setOpen(false);
|
||||
if (!isOpen) this.open(activeIndex);
|
||||
else setIsOpen(false);
|
||||
},
|
||||
open(activeIndex?: number) {
|
||||
if (activeIndex === undefined) {
|
||||
@@ -81,7 +90,7 @@ export const Dropdown = forwardRef<DropdownRef, DropdownProps>(function Dropdown
|
||||
} else {
|
||||
setDefaultSelectedIndex(activeIndex >= 0 ? activeIndex : items.length + activeIndex);
|
||||
}
|
||||
setOpen(true);
|
||||
setIsOpen(true);
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -98,54 +107,97 @@ export const Dropdown = forwardRef<DropdownRef, DropdownProps>(function Dropdown
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setDefaultSelectedIndex(undefined);
|
||||
setOpen((o) => !o);
|
||||
setIsOpen((o) => !o);
|
||||
}),
|
||||
};
|
||||
return cloneElement(existingChild, props);
|
||||
}, [children]);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
setOpen(false);
|
||||
setIsOpen(false);
|
||||
buttonRef.current?.focus();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
buttonRef.current?.setAttribute('aria-expanded', open.toString());
|
||||
}, [open]);
|
||||
buttonRef.current?.setAttribute('aria-expanded', isOpen.toString());
|
||||
}, [isOpen]);
|
||||
|
||||
const windowSize = useWindowSize();
|
||||
const triggerRect = useMemo(() => {
|
||||
if (!windowSize) return null; // No-op to TS happy with this dep
|
||||
if (!open) return null;
|
||||
if (!isOpen) return null;
|
||||
return buttonRef.current?.getBoundingClientRect();
|
||||
}, [open, windowSize]);
|
||||
}, [isOpen, windowSize]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{child}
|
||||
{open && triggerRect && (
|
||||
<Menu
|
||||
ref={menuRef}
|
||||
defaultSelectedIndex={defaultSelectedIndex}
|
||||
items={items}
|
||||
triggerRect={triggerRect}
|
||||
onClose={handleClose}
|
||||
/>
|
||||
)}
|
||||
<Menu
|
||||
ref={menuRef}
|
||||
showTriangle
|
||||
defaultSelectedIndex={defaultSelectedIndex}
|
||||
items={items}
|
||||
triggerShape={triggerRect ?? null}
|
||||
onClose={handleClose}
|
||||
isOpen={isOpen}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
interface ContextMenuProps {
|
||||
show: { x: number; y: number } | null;
|
||||
className?: string;
|
||||
items: DropdownProps['items'];
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const ContextMenu = forwardRef<DropdownRef, ContextMenuProps>(function ContextMenu(
|
||||
{ show, className, items, onClose },
|
||||
ref,
|
||||
) {
|
||||
const triggerShape = useMemo(
|
||||
() => ({
|
||||
top: show?.y ?? 0,
|
||||
bottom: show?.y ?? 0,
|
||||
left: show?.x ?? 0,
|
||||
right: show?.x ?? 0,
|
||||
}),
|
||||
[show],
|
||||
);
|
||||
|
||||
return (
|
||||
<Menu
|
||||
className={className}
|
||||
ref={ref}
|
||||
items={items}
|
||||
isOpen={show != null}
|
||||
onClose={onClose}
|
||||
triggerShape={triggerShape}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
interface MenuProps {
|
||||
className?: string;
|
||||
defaultSelectedIndex?: number;
|
||||
items: DropdownProps['items'];
|
||||
triggerRect: DOMRect;
|
||||
triggerShape: Pick<DOMRect, 'top' | 'bottom' | 'left' | 'right'> | null;
|
||||
onClose: () => void;
|
||||
showTriangle?: boolean;
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle'>, MenuProps>(function Menu(
|
||||
{ className, items, onClose, triggerRect, defaultSelectedIndex }: MenuProps,
|
||||
{
|
||||
className,
|
||||
isOpen,
|
||||
items,
|
||||
onClose,
|
||||
triggerShape,
|
||||
defaultSelectedIndex,
|
||||
showTriangle,
|
||||
}: MenuProps,
|
||||
ref,
|
||||
) {
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
@@ -248,21 +300,29 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle'>, MenuPro
|
||||
|
||||
const { containerStyles, triangleStyles } = useMemo<{
|
||||
containerStyles: CSSProperties;
|
||||
triangleStyles: CSSProperties;
|
||||
triangleStyles: CSSProperties | null;
|
||||
}>(() => {
|
||||
const docWidth = document.documentElement.getBoundingClientRect().width;
|
||||
const spaceRemaining = docWidth - triggerRect.left;
|
||||
const top = triggerRect?.bottom + 5;
|
||||
const onRight = spaceRemaining < 200;
|
||||
const containerStyles = onRight
|
||||
? { top, right: docWidth - triggerRect?.right }
|
||||
: { top, left: triggerRect?.left };
|
||||
if (triggerShape == null) return { containerStyles: {}, triangleStyles: null };
|
||||
|
||||
const docRect = document.documentElement.getBoundingClientRect();
|
||||
const width = triggerShape.right - triggerShape.left;
|
||||
const hSpaceRemaining = docRect.width - triggerShape.left;
|
||||
const vSpaceRemaining = docRect.height - triggerShape.bottom;
|
||||
const top = triggerShape?.bottom + 5;
|
||||
const onRight = hSpaceRemaining < 200;
|
||||
const upsideDown = vSpaceRemaining < 200;
|
||||
const containerStyles = {
|
||||
top: !upsideDown ? top : undefined,
|
||||
bottom: upsideDown ? docRect.height - top : undefined,
|
||||
right: onRight ? docRect.width - triggerShape?.right : undefined,
|
||||
left: !onRight ? triggerShape?.left : undefined,
|
||||
};
|
||||
const size = { top: '-0.2rem', width: '0.4rem', height: '0.4rem' };
|
||||
const triangleStyles = onRight
|
||||
? { right: triggerRect.width / 2, marginRight: '-0.2rem', ...size }
|
||||
: { left: triggerRect.width / 2, marginLeft: '-0.2rem', ...size };
|
||||
? { right: width / 2, marginRight: '-0.2rem', ...size }
|
||||
: { left: width / 2, marginLeft: '-0.2rem', ...size };
|
||||
return { containerStyles, triangleStyles };
|
||||
}, [triggerRect]);
|
||||
}, [triggerShape]);
|
||||
|
||||
const handleFocus = useCallback(
|
||||
(i: DropdownItem) => {
|
||||
@@ -275,67 +335,84 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle'>, MenuPro
|
||||
if (items.length === 0) return null;
|
||||
|
||||
return (
|
||||
<Overlay open variant="transparent" portalName="dropdown" zIndex={50}>
|
||||
<div>
|
||||
<div tabIndex={-1} aria-hidden className="fixed inset-0 z-30" onClick={onClose} />
|
||||
<motion.div
|
||||
tabIndex={0}
|
||||
onKeyDown={handleMenuKeyDown}
|
||||
initial={{ opacity: 0, y: -5, scale: 0.98 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
role="menu"
|
||||
aria-orientation="vertical"
|
||||
dir="ltr"
|
||||
ref={containerRef}
|
||||
style={containerStyles}
|
||||
className={classNames(className, 'outline-none mt-1 pointer-events-auto fixed z-50')}
|
||||
>
|
||||
<span
|
||||
aria-hidden
|
||||
style={triangleStyles}
|
||||
className="bg-gray-50 absolute rotate-45 border-gray-200 border-t border-l"
|
||||
/>
|
||||
{containerStyles && (
|
||||
<VStack
|
||||
space={0.5}
|
||||
ref={initMenu}
|
||||
style={menuStyles}
|
||||
className={classNames(
|
||||
className,
|
||||
'h-auto bg-gray-50 rounded-md shadow-lg dark:shadow-gray-0 py-1.5 border',
|
||||
'border-gray-200 overflow-auto mb-1 mx-0.5',
|
||||
)}
|
||||
<>
|
||||
{items.map(
|
||||
(item) =>
|
||||
item.type !== 'separator' && (
|
||||
<MenuItemHotKey
|
||||
key={item.key}
|
||||
onSelect={handleSelect}
|
||||
item={item}
|
||||
action={item.hotkeyAction}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
{isOpen && (
|
||||
<Overlay open variant="transparent" portalName="dropdown" zIndex={50}>
|
||||
<div>
|
||||
<div tabIndex={-1} aria-hidden className="fixed inset-0 z-30" onClick={onClose} />
|
||||
<motion.div
|
||||
tabIndex={0}
|
||||
onKeyDown={handleMenuKeyDown}
|
||||
initial={{ opacity: 0, y: -5, scale: 0.98 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
role="menu"
|
||||
aria-orientation="vertical"
|
||||
dir="ltr"
|
||||
ref={containerRef}
|
||||
style={containerStyles}
|
||||
className={classNames(className, 'outline-none my-1 pointer-events-auto fixed z-50')}
|
||||
>
|
||||
{items.map((item, i) => {
|
||||
if (item.type === 'separator') {
|
||||
return <Separator key={i} className="my-1.5" label={item.label} />;
|
||||
}
|
||||
if (item.hidden) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<MenuItem
|
||||
focused={i === selectedIndex}
|
||||
onFocus={handleFocus}
|
||||
onSelect={handleSelect}
|
||||
key={item.key}
|
||||
item={item}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</VStack>
|
||||
)}
|
||||
</motion.div>
|
||||
</div>
|
||||
</Overlay>
|
||||
{triangleStyles && showTriangle && (
|
||||
<span
|
||||
aria-hidden
|
||||
style={triangleStyles}
|
||||
className="bg-gray-50 absolute rotate-45 border-gray-200 border-t border-l"
|
||||
/>
|
||||
)}
|
||||
{containerStyles && (
|
||||
<VStack
|
||||
space={0.5}
|
||||
ref={initMenu}
|
||||
style={menuStyles}
|
||||
className={classNames(
|
||||
className,
|
||||
'h-auto bg-gray-50 rounded-md shadow-lg dark:shadow-gray-0 py-1.5 border',
|
||||
'border-gray-200 overflow-auto mb-1 mx-0.5',
|
||||
)}
|
||||
>
|
||||
{items.map((item, i) => {
|
||||
if (item.type === 'separator') {
|
||||
return <Separator key={i} className="my-1.5" label={item.label} />;
|
||||
}
|
||||
if (item.hidden) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<MenuItem
|
||||
focused={i === selectedIndex}
|
||||
onFocus={handleFocus}
|
||||
onSelect={handleSelect}
|
||||
key={item.key}
|
||||
item={item}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</VStack>
|
||||
)}
|
||||
</motion.div>
|
||||
</div>
|
||||
</Overlay>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
interface MenuItemProps {
|
||||
className?: string;
|
||||
item: DropdownItem;
|
||||
onSelect: (item: DropdownItem) => void;
|
||||
onFocus: (item: DropdownItem) => void;
|
||||
item: DropdownItemDefault;
|
||||
onSelect: (item: DropdownItemDefault) => void;
|
||||
onFocus: (item: DropdownItemDefault) => void;
|
||||
focused: boolean;
|
||||
}
|
||||
|
||||
@@ -359,7 +436,7 @@ function MenuItem({ className, focused, onFocus, item, onSelect, ...props }: Men
|
||||
[focused],
|
||||
);
|
||||
|
||||
if (item.type === 'separator') return <Separator className="my-1.5" />;
|
||||
const rightSlot = item.rightSlot ?? <HotKey action={item.hotkeyAction ?? null} />;
|
||||
|
||||
return (
|
||||
<Button
|
||||
@@ -373,7 +450,7 @@ function MenuItem({ className, focused, onFocus, item, onSelect, ...props }: Men
|
||||
onClick={handleClick}
|
||||
justify="start"
|
||||
leftSlot={item.leftSlot && <div className="pr-2 flex justify-start">{item.leftSlot}</div>}
|
||||
rightSlot={item.rightSlot && <div className="ml-auto pl-3">{item.rightSlot}</div>}
|
||||
rightSlot={rightSlot && <div className="ml-auto pl-3">{rightSlot}</div>}
|
||||
className={classNames(
|
||||
className,
|
||||
'min-w-[8rem] outline-none px-2 mx-1.5 flex text-sm text-gray-700 whitespace-nowrap',
|
||||
@@ -394,3 +471,14 @@ function MenuItem({ className, focused, onFocus, item, onSelect, ...props }: Men
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
interface MenuItemHotKeyProps {
|
||||
action: HotkeyAction | undefined;
|
||||
onSelect: MenuItemProps['onSelect'];
|
||||
item: MenuItemProps['item'];
|
||||
}
|
||||
|
||||
function MenuItemHotKey({ action, onSelect, item }: MenuItemHotKeyProps) {
|
||||
useHotKey(action ?? null, () => onSelect(item));
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -290,6 +290,8 @@ function getExtensions({
|
||||
|
||||
// Handle onChange
|
||||
EditorView.updateListener.of((update) => {
|
||||
// Only fire onChange if the document changed and the update was from user input. This prevents firing onChange when the document is updated when
|
||||
// changing pages (one request to another in header editor)
|
||||
if (onChange && update.docChanged && isViewUpdateFromUserInput(update)) {
|
||||
onChange.current?.(update.state.doc.toString());
|
||||
}
|
||||
|
||||
@@ -1,21 +1,35 @@
|
||||
import classNames from 'classnames';
|
||||
import type { HotkeyAction } from '../../hooks/useHotKey';
|
||||
import { useFormattedHotkey } from '../../hooks/useHotKey';
|
||||
import { useOsInfo } from '../../hooks/useOsInfo';
|
||||
import { HStack } from './Stacks';
|
||||
|
||||
interface Props {
|
||||
modifier: 'Meta' | 'Control' | 'Shift';
|
||||
keyName: string;
|
||||
action: HotkeyAction | null;
|
||||
className?: string;
|
||||
variant?: 'text' | 'with-bg';
|
||||
}
|
||||
|
||||
const keys: Record<Props['modifier'], string> = {
|
||||
Control: '⌃',
|
||||
Meta: '⌘',
|
||||
Shift: '⇧',
|
||||
};
|
||||
export function HotKey({ action, className, variant }: Props) {
|
||||
const osInfo = useOsInfo();
|
||||
const labelParts = useFormattedHotkey(action);
|
||||
if (labelParts === null || osInfo == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
export function HotKey({ modifier, keyName }: Props) {
|
||||
return (
|
||||
<span className={classNames('text-sm text-gray-600')}>
|
||||
{keys[modifier]}
|
||||
{keyName}
|
||||
</span>
|
||||
<HStack
|
||||
className={classNames(
|
||||
className,
|
||||
variant === 'with-bg' && 'rounded border',
|
||||
'text-gray-1000 text-opacity-disabled',
|
||||
)}
|
||||
>
|
||||
{labelParts.map((char, index) => (
|
||||
<div key={index} className="min-w-[1.1em] text-center">
|
||||
{char}
|
||||
</div>
|
||||
))}
|
||||
</HStack>
|
||||
);
|
||||
}
|
||||
|
||||
11
src-web/components/core/HotKeyLabel.tsx
Normal file
11
src-web/components/core/HotKeyLabel.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import type { HotkeyAction } from '../../hooks/useHotKey';
|
||||
import { useHotKeyLabel } from '../../hooks/useHotKey';
|
||||
|
||||
interface Props {
|
||||
action: HotkeyAction;
|
||||
}
|
||||
|
||||
export function HotKeyLabel({ action }: Props) {
|
||||
const label = useHotKeyLabel(action);
|
||||
return <span>{label}</span>;
|
||||
}
|
||||
24
src-web/components/core/HotKeyList.tsx
Normal file
24
src-web/components/core/HotKeyList.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
import type { HotkeyAction } from '../../hooks/useHotKey';
|
||||
import { HotKey } from './HotKey';
|
||||
import { HotKeyLabel } from './HotKeyLabel';
|
||||
import { HStack, VStack } from './Stacks';
|
||||
|
||||
interface Props {
|
||||
hotkeys: HotkeyAction[];
|
||||
}
|
||||
|
||||
export const HotKeyList = ({ hotkeys }: Props) => {
|
||||
return (
|
||||
<div className="mx-auto h-full flex items-center text-gray-700 text-sm">
|
||||
<VStack space={2}>
|
||||
{hotkeys.map((hotkey) => (
|
||||
<HStack key={hotkey} className="grid grid-cols-2">
|
||||
<HotKeyLabel action={hotkey} />
|
||||
<HotKey className="ml-auto" action={hotkey} />
|
||||
</HStack>
|
||||
))}
|
||||
</VStack>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,44 +1,4 @@
|
||||
import {
|
||||
ArchiveIcon,
|
||||
CameraIcon,
|
||||
CheckboxIcon,
|
||||
CheckIcon,
|
||||
ChevronDownIcon,
|
||||
ChevronRightIcon,
|
||||
ClockIcon,
|
||||
CodeIcon,
|
||||
ColorWheelIcon,
|
||||
CopyIcon,
|
||||
Cross2Icon,
|
||||
DividerHorizontalIcon,
|
||||
DotsHorizontalIcon,
|
||||
DotsVerticalIcon,
|
||||
DownloadIcon,
|
||||
DragHandleDots2Icon,
|
||||
EyeClosedIcon,
|
||||
EyeOpenIcon,
|
||||
GearIcon,
|
||||
HamburgerMenuIcon,
|
||||
HomeIcon,
|
||||
ListBulletIcon,
|
||||
MagicWandIcon,
|
||||
MagnifyingGlassIcon,
|
||||
MoonIcon,
|
||||
OpenInNewWindowIcon,
|
||||
PaperPlaneIcon,
|
||||
Pencil2Icon,
|
||||
PlusCircledIcon,
|
||||
PlusIcon,
|
||||
QuestionMarkIcon,
|
||||
RowsIcon,
|
||||
SunIcon,
|
||||
TrashIcon,
|
||||
TriangleDownIcon,
|
||||
TriangleLeftIcon,
|
||||
TriangleRightIcon,
|
||||
UpdateIcon,
|
||||
UploadIcon,
|
||||
} from '@radix-ui/react-icons';
|
||||
import * as I from '@radix-ui/react-icons';
|
||||
import classNames from 'classnames';
|
||||
import type { HTMLAttributes } from 'react';
|
||||
import { memo } from 'react';
|
||||
@@ -46,47 +6,53 @@ import { ReactComponent as LeftPanelHiddenIcon } from '../../assets/icons/LeftPa
|
||||
import { ReactComponent as LeftPanelVisibleIcon } from '../../assets/icons/LeftPanelVisibleIcon.svg';
|
||||
|
||||
const icons = {
|
||||
archive: ArchiveIcon,
|
||||
camera: CameraIcon,
|
||||
check: CheckIcon,
|
||||
checkbox: CheckboxIcon,
|
||||
clock: ClockIcon,
|
||||
chevronDown: ChevronDownIcon,
|
||||
chevronRight: ChevronRightIcon,
|
||||
code: CodeIcon,
|
||||
colorWheel: ColorWheelIcon,
|
||||
copy: CopyIcon,
|
||||
dividerH: DividerHorizontalIcon,
|
||||
dotsH: DotsHorizontalIcon,
|
||||
dotsV: DotsVerticalIcon,
|
||||
download: DownloadIcon,
|
||||
drag: DragHandleDots2Icon,
|
||||
eye: EyeOpenIcon,
|
||||
eyeClosed: EyeClosedIcon,
|
||||
gear: GearIcon,
|
||||
hamburger: HamburgerMenuIcon,
|
||||
home: HomeIcon,
|
||||
archive: I.ArchiveIcon,
|
||||
chat: I.ChatBubbleIcon,
|
||||
check: I.CheckIcon,
|
||||
checkbox: I.CheckboxIcon,
|
||||
chevronDown: I.ChevronDownIcon,
|
||||
chevronRight: I.ChevronRightIcon,
|
||||
clock: I.ClockIcon,
|
||||
code: I.CodeIcon,
|
||||
colorWheel: I.ColorWheelIcon,
|
||||
copy: I.CopyIcon,
|
||||
dividerH: I.DividerHorizontalIcon,
|
||||
dotsH: I.DotsHorizontalIcon,
|
||||
dotsV: I.DotsVerticalIcon,
|
||||
download: I.DownloadIcon,
|
||||
drag: I.DragHandleDots2Icon,
|
||||
eye: I.EyeOpenIcon,
|
||||
eyeClosed: I.EyeClosedIcon,
|
||||
gear: I.GearIcon,
|
||||
hamburger: I.HamburgerMenuIcon,
|
||||
home: I.HomeIcon,
|
||||
keyboard: I.KeyboardIcon,
|
||||
listBullet: I.ListBulletIcon,
|
||||
magicWand: I.MagicWandIcon,
|
||||
magnifyingGlass: I.MagnifyingGlassIcon,
|
||||
minus: I.MinusIcon,
|
||||
moon: I.MoonIcon,
|
||||
openNewWindow: I.OpenInNewWindowIcon,
|
||||
paperPlane: I.PaperPlaneIcon,
|
||||
pencil: I.Pencil2Icon,
|
||||
plus: I.PlusIcon,
|
||||
plusCircle: I.PlusCircledIcon,
|
||||
question: I.QuestionMarkIcon,
|
||||
rocket: I.RocketIcon,
|
||||
rows: I.RowsIcon,
|
||||
square: I.SquareIcon,
|
||||
sun: I.SunIcon,
|
||||
trash: I.TrashIcon,
|
||||
triangleDown: I.TriangleDownIcon,
|
||||
triangleLeft: I.TriangleLeftIcon,
|
||||
triangleRight: I.TriangleRightIcon,
|
||||
update: I.UpdateIcon,
|
||||
upload: I.UploadIcon,
|
||||
x: I.Cross2Icon,
|
||||
|
||||
// Custom
|
||||
leftPanelHidden: LeftPanelHiddenIcon,
|
||||
leftPanelVisible: LeftPanelVisibleIcon,
|
||||
listBullet: ListBulletIcon,
|
||||
magicWand: MagicWandIcon,
|
||||
magnifyingGlass: MagnifyingGlassIcon,
|
||||
moon: MoonIcon,
|
||||
openNewWindow: OpenInNewWindowIcon,
|
||||
paperPlane: PaperPlaneIcon,
|
||||
pencil: Pencil2Icon,
|
||||
plus: PlusIcon,
|
||||
plusCircle: PlusCircledIcon,
|
||||
question: QuestionMarkIcon,
|
||||
rows: RowsIcon,
|
||||
sun: SunIcon,
|
||||
trash: TrashIcon,
|
||||
triangleDown: TriangleDownIcon,
|
||||
triangleLeft: TriangleLeftIcon,
|
||||
triangleRight: TriangleRightIcon,
|
||||
update: UpdateIcon,
|
||||
upload: UploadIcon,
|
||||
x: Cross2Icon,
|
||||
empty: (props: HTMLAttributes<HTMLSpanElement>) => <span {...props} />,
|
||||
};
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ export const IconButton = forwardRef<HTMLButtonElement, Props>(function IconButt
|
||||
},
|
||||
[onClick, setConfirmed, showConfirm],
|
||||
);
|
||||
|
||||
return (
|
||||
<Button
|
||||
ref={ref}
|
||||
|
||||
@@ -335,7 +335,8 @@ const FormRow = memo(function FormRow({
|
||||
<span className="w-3" />
|
||||
)}
|
||||
<Checkbox
|
||||
title={pairContainer.pair.enabled ? 'disable entry' : 'Enable item'}
|
||||
hideLabel
|
||||
title={pairContainer.pair.enabled ? 'Disable item' : 'Enable item'}
|
||||
disabled={isLast}
|
||||
checked={isLast ? false : !!pairContainer.pair.enabled}
|
||||
className={classNames('mr-2', isLast && '!opacity-disabled')}
|
||||
@@ -426,7 +427,7 @@ const FormRow = memo(function FormRow({
|
||||
size="sm"
|
||||
title="Delete header"
|
||||
onClick={!isLast ? handleDelete : undefined}
|
||||
className="ml-0.5 group-hover:!opacity-100 focus-visible:!opacity-100"
|
||||
className="ml-0.5 opacity-0 group-hover:!opacity-100 focus-visible:!opacity-100"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -54,7 +54,7 @@ export const VStack = forwardRef(function VStack(
|
||||
});
|
||||
|
||||
type BaseStackProps = HTMLAttributes<HTMLElement> & {
|
||||
as?: ComponentType | 'ul' | 'form';
|
||||
as?: ComponentType | 'ul' | 'label' | 'form';
|
||||
space?: keyof typeof gapClasses;
|
||||
alignItems?: 'start' | 'center' | 'stretch';
|
||||
justifyContent?: 'start' | 'center' | 'end' | 'between';
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { convertFileSrc } from '@tauri-apps/api/tauri';
|
||||
import classNames from 'classnames';
|
||||
import { useState } from 'react';
|
||||
import type { HttpResponse } from '../../lib/models';
|
||||
|
||||
interface Props {
|
||||
@@ -8,11 +9,31 @@ interface Props {
|
||||
}
|
||||
|
||||
export function ImageViewer({ response, className }: Props) {
|
||||
const bytes = response.contentLength ?? 0;
|
||||
const [show, setShow] = useState(bytes < 3 * 1000 * 1000);
|
||||
|
||||
if (response.bodyPath === null) {
|
||||
return <div>Empty response body</div>;
|
||||
}
|
||||
|
||||
const src = convertFileSrc(response.bodyPath);
|
||||
if (!show) {
|
||||
return (
|
||||
<>
|
||||
<div className="text-sm italic text-gray-500">
|
||||
Response body is too large to preview.{' '}
|
||||
<button
|
||||
className="cursor-pointer underline hover:text-gray-800"
|
||||
color="gray"
|
||||
onClick={() => setShow(true)}
|
||||
>
|
||||
Show anyway
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<img
|
||||
src={src}
|
||||
|
||||
@@ -20,6 +20,7 @@ export function WebPageViewer({ response }: Props) {
|
||||
return (
|
||||
<div className="h-full pb-3">
|
||||
<iframe
|
||||
key={body ? 'has-body' : 'no-body'}
|
||||
title="Response preview"
|
||||
srcDoc={contentForIframe}
|
||||
sandbox="allow-scripts allow-same-origin"
|
||||
|
||||
@@ -13,8 +13,6 @@ export function useCreateEnvironment() {
|
||||
const prompt = usePrompt();
|
||||
const workspaceId = useActiveWorkspaceId();
|
||||
const queryClient = useQueryClient();
|
||||
const environments = useEnvironments();
|
||||
const workspaces = useWorkspaces();
|
||||
|
||||
return useMutation<Environment, unknown, void>({
|
||||
mutationFn: async () => {
|
||||
@@ -24,13 +22,9 @@ export function useCreateEnvironment() {
|
||||
label: 'Name',
|
||||
defaultValue: 'My Environment',
|
||||
});
|
||||
const variables =
|
||||
environments.length === 0 && workspaces.length === 1
|
||||
? [{ name: 'first_variable', value: 'some reusable value' }]
|
||||
: [];
|
||||
return invoke('create_environment', { name, variables, workspaceId });
|
||||
return invoke('create_environment', { name, variables: [], workspaceId });
|
||||
},
|
||||
onSettled: () => trackEvent('environment', 'create'),
|
||||
onSettled: () => trackEvent('Environment', 'Create'),
|
||||
onSuccess: async (environment) => {
|
||||
if (workspaceId == null) return;
|
||||
routes.setEnvironment(environment);
|
||||
|
||||
@@ -15,10 +15,10 @@ export function useCreateFolder() {
|
||||
throw new Error("Cannot create folder when there's no active workspace");
|
||||
}
|
||||
patch.name = patch.name || 'New Folder';
|
||||
patch.sortPriority = patch.sortPriority || Date.now();
|
||||
patch.sortPriority = patch.sortPriority || -Date.now();
|
||||
return invoke('create_folder', { workspaceId, ...patch });
|
||||
},
|
||||
onSettled: () => trackEvent('folder', 'create'),
|
||||
onSettled: () => trackEvent('Folder', 'Create'),
|
||||
onSuccess: async (request) => {
|
||||
await queryClient.invalidateQueries(foldersQueryKey({ workspaceId: request.workspaceId }));
|
||||
},
|
||||
|
||||
@@ -3,15 +3,16 @@ import { invoke } from '@tauri-apps/api';
|
||||
import { trackEvent } from '../lib/analytics';
|
||||
import type { HttpRequest } from '../lib/models';
|
||||
import { useActiveEnvironmentId } from './useActiveEnvironmentId';
|
||||
import { useActiveRequest } from './useActiveRequest';
|
||||
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
|
||||
import { useAppRoutes } from './useAppRoutes';
|
||||
import { requestsQueryKey, useRequests } from './useRequests';
|
||||
import { requestsQueryKey } from './useRequests';
|
||||
|
||||
export function useCreateRequest() {
|
||||
const workspaceId = useActiveWorkspaceId();
|
||||
const activeEnvironmentId = useActiveEnvironmentId();
|
||||
const activeRequest = useActiveRequest();
|
||||
const routes = useAppRoutes();
|
||||
const requests = useRequests();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<
|
||||
@@ -23,10 +24,11 @@ export function useCreateRequest() {
|
||||
if (workspaceId === null) {
|
||||
throw new Error("Cannot create request when there's no active workspace");
|
||||
}
|
||||
patch.sortPriority = patch.sortPriority || maxSortPriority(requests) + 1000;
|
||||
patch.sortPriority = patch.sortPriority || -Date.now();
|
||||
patch.folderId = patch.folderId || activeRequest?.folderId;
|
||||
return invoke('create_request', { workspaceId, name: '', ...patch });
|
||||
},
|
||||
onSettled: () => trackEvent('http_request', 'create'),
|
||||
onSettled: () => trackEvent('HttpRequest', 'Create'),
|
||||
onSuccess: async (request) => {
|
||||
queryClient.setQueryData<HttpRequest[]>(
|
||||
requestsQueryKey({ workspaceId: request.workspaceId }),
|
||||
@@ -40,8 +42,3 @@ export function useCreateRequest() {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function maxSortPriority(requests: HttpRequest[]) {
|
||||
if (requests.length === 0) return 1000;
|
||||
return Math.max(...requests.map((r) => r.sortPriority));
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ export function useCreateWorkspace({ navigateAfter }: { navigateAfter: boolean }
|
||||
mutationFn: (patch) => {
|
||||
return invoke('create_workspace', patch);
|
||||
},
|
||||
onSettled: () => trackEvent('workspace', 'create'),
|
||||
onSettled: () => trackEvent('Workspace', 'Create'),
|
||||
onSuccess: async (workspace) => {
|
||||
queryClient.setQueryData<Workspace[]>(workspacesQueryKey({}), (workspaces) => [
|
||||
...(workspaces ?? []),
|
||||
|
||||
@@ -28,7 +28,7 @@ export function useDeleteAnyRequest() {
|
||||
if (!confirmed) return null;
|
||||
return invoke('delete_request', { requestId: id });
|
||||
},
|
||||
onSettled: () => trackEvent('http_request', 'delete'),
|
||||
onSettled: () => trackEvent('HttpRequest', 'Delete'),
|
||||
onSuccess: async (request) => {
|
||||
// Was it cancelled?
|
||||
if (request === null) return;
|
||||
|
||||
@@ -24,7 +24,7 @@ export function useDeleteEnvironment(environment: Environment | null) {
|
||||
if (!confirmed) return null;
|
||||
return invoke('delete_environment', { environmentId: environment?.id });
|
||||
},
|
||||
onSettled: () => trackEvent('environment', 'delete'),
|
||||
onSettled: () => trackEvent('Environment', 'Delete'),
|
||||
onSuccess: async (environment) => {
|
||||
if (environment === null) return;
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ export function useDeleteFolder(id: string | null) {
|
||||
if (!confirmed) return null;
|
||||
return invoke('delete_folder', { folderId: id });
|
||||
},
|
||||
onSettled: () => trackEvent('folder', 'delete'),
|
||||
onSettled: () => trackEvent('Folder', 'Delete'),
|
||||
onSuccess: async (folder) => {
|
||||
// Was it cancelled?
|
||||
if (folder === null) return;
|
||||
|
||||
@@ -10,7 +10,7 @@ export function useDeleteResponse(id: string | null) {
|
||||
mutationFn: async () => {
|
||||
return await invoke('delete_response', { id: id });
|
||||
},
|
||||
onSettled: () => trackEvent('http_response', 'delete'),
|
||||
onSettled: () => trackEvent('HttpResponse', 'Delete'),
|
||||
onSuccess: ({ requestId, id: responseId }) => {
|
||||
queryClient.setQueryData<HttpResponse[]>(responsesQueryKey({ requestId }), (responses) =>
|
||||
(responses ?? []).filter((response) => response.id !== responseId),
|
||||
|
||||
@@ -10,7 +10,7 @@ export function useDeleteResponses(requestId?: string) {
|
||||
if (requestId === undefined) return;
|
||||
await invoke('delete_all_responses', { requestId });
|
||||
},
|
||||
onSettled: () => trackEvent('http_response', 'delete_many'),
|
||||
onSettled: () => trackEvent('HttpResponse', 'DeleteMany'),
|
||||
onSuccess: async () => {
|
||||
if (requestId === undefined) return;
|
||||
queryClient.setQueryData(responsesQueryKey({ requestId }), []);
|
||||
|
||||
@@ -29,7 +29,7 @@ export function useDeleteWorkspace(workspace: Workspace | null) {
|
||||
if (!confirmed) return null;
|
||||
return invoke('delete_workspace', { workspaceId: workspace?.id });
|
||||
},
|
||||
onSettled: () => trackEvent('workspace', 'delete'),
|
||||
onSettled: () => trackEvent('Workspace', 'Delete'),
|
||||
onSuccess: async (workspace) => {
|
||||
if (workspace === null) return;
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ export function useDuplicateRequest({
|
||||
if (id === null) throw new Error("Can't duplicate a null request");
|
||||
return invoke('duplicate_request', { id });
|
||||
},
|
||||
onSettled: () => trackEvent('http_request', 'duplicate'),
|
||||
onSettled: () => trackEvent('HttpRequest', 'Duplicate'),
|
||||
onSuccess: async (request) => {
|
||||
queryClient.setQueryData<HttpRequest[]>(
|
||||
requestsQueryKey({ workspaceId: request.workspaceId }),
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import type { SaveDialogOptions } from '@tauri-apps/api/dialog';
|
||||
import { save } from '@tauri-apps/api/dialog';
|
||||
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
|
||||
import slugify from 'slugify';
|
||||
import { useActiveWorkspace } from './useActiveWorkspace';
|
||||
import { useAlert } from './useAlert';
|
||||
|
||||
const saveArgs: SaveDialogOptions = {
|
||||
title: 'Export Data',
|
||||
defaultPath: 'yaak-export.json',
|
||||
};
|
||||
|
||||
export function useExportData() {
|
||||
const workspaceId = useActiveWorkspaceId();
|
||||
const workspace = useActiveWorkspace();
|
||||
const alert = useAlert();
|
||||
|
||||
return useMutation({
|
||||
@@ -19,12 +14,18 @@ export function useExportData() {
|
||||
alert({ title: 'Export Failed', body: err });
|
||||
},
|
||||
mutationFn: async () => {
|
||||
const exportPath = await save(saveArgs);
|
||||
if (workspace == null) return;
|
||||
|
||||
const workspaceSlug = slugify(workspace.name, { lower: true });
|
||||
const exportPath = await save({
|
||||
title: 'Export Data',
|
||||
defaultPath: `yaak.${workspaceSlug}.json`,
|
||||
});
|
||||
if (exportPath == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
await invoke('export_data', { workspaceId, exportPath });
|
||||
await invoke('export_data', { workspaceId: workspace.id, exportPath });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
173
src-web/hooks/useHotKey.ts
Normal file
173
src-web/hooks/useHotKey.ts
Normal file
@@ -0,0 +1,173 @@
|
||||
import type {OsType} from '@tauri-apps/api/os';
|
||||
import {useEffect, useRef} from 'react';
|
||||
import {capitalize} from '../lib/capitalize';
|
||||
import {debounce} from '../lib/debounce';
|
||||
import {useOsInfo} from './useOsInfo';
|
||||
|
||||
export type HotkeyAction =
|
||||
| 'request.send'
|
||||
| 'request.create'
|
||||
| 'request.duplicate'
|
||||
| 'sidebar.toggle'
|
||||
| 'sidebar.focus'
|
||||
| 'urlBar.focus'
|
||||
| 'environmentEditor.toggle'
|
||||
| 'hotkeys.showHelp'
|
||||
| 'requestSwitcher.prev'
|
||||
| 'requestSwitcher.next'
|
||||
| 'settings.show';
|
||||
|
||||
const hotkeys: Record<HotkeyAction, string[]> = {
|
||||
'request.send': ['CmdCtrl+Enter', 'CmdCtrl+r'],
|
||||
'request.create': ['CmdCtrl+n'],
|
||||
'request.duplicate': ['CmdCtrl+d'],
|
||||
'sidebar.toggle': ['CmdCtrl+b'],
|
||||
'sidebar.focus': ['CmdCtrl+1'],
|
||||
'urlBar.focus': ['CmdCtrl+l'],
|
||||
'environmentEditor.toggle': ['CmdCtrl+e'],
|
||||
'hotkeys.showHelp': ['CmdCtrl+/'],
|
||||
'settings.show': ['CmdCtrl+,'],
|
||||
'requestSwitcher.prev': ['Control+Tab'],
|
||||
'requestSwitcher.next': ['Control+Shift+Tab'],
|
||||
};
|
||||
|
||||
const hotkeyLabels: Record<HotkeyAction, string> = {
|
||||
'request.send': 'Send Request',
|
||||
'request.create': 'New Request',
|
||||
'request.duplicate': 'Duplicate Request',
|
||||
'sidebar.toggle': 'Toggle Sidebar',
|
||||
'sidebar.focus': 'Focus Sidebar',
|
||||
'urlBar.focus': 'Focus URL',
|
||||
'environmentEditor.toggle': 'Edit Environments',
|
||||
'hotkeys.showHelp': 'Show Keyboard Shortcuts',
|
||||
'requestSwitcher.prev': 'Go To Next Request',
|
||||
'requestSwitcher.next': 'Go To Previous Request',
|
||||
'settings.show': 'Open Settings',
|
||||
};
|
||||
|
||||
export const hotkeyActions: HotkeyAction[] = Object.keys(hotkeys) as (keyof typeof hotkeys)[];
|
||||
|
||||
interface Options {
|
||||
enable?: boolean;
|
||||
}
|
||||
|
||||
export function useHotKey(
|
||||
action: HotkeyAction | null,
|
||||
callback: (e: KeyboardEvent) => void,
|
||||
options: Options = {},
|
||||
) {
|
||||
useAnyHotkey((hkAction, e) => {
|
||||
if (hkAction === action) {
|
||||
callback(e);
|
||||
}
|
||||
}, options);
|
||||
}
|
||||
|
||||
export function useAnyHotkey(
|
||||
callback: (action: HotkeyAction, e: KeyboardEvent) => void,
|
||||
options: Options,
|
||||
) {
|
||||
const currentKeys = useRef<Set<string>>(new Set());
|
||||
const callbackRef = useRef(callback);
|
||||
const osInfo = useOsInfo();
|
||||
const os = osInfo?.osType ?? null;
|
||||
|
||||
useEffect(() => {
|
||||
callbackRef.current = callback;
|
||||
}, [callback]);
|
||||
|
||||
useEffect(() => {
|
||||
// Sometimes the keyup event doesn't fire, so we clear the keys after a timeout
|
||||
const clearCurrentKeys = debounce(() => currentKeys.current.clear(), 1000);
|
||||
|
||||
const down = (e: KeyboardEvent) => {
|
||||
if (options.enable === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
currentKeys.current.add(normalizeKey(e.key, os));
|
||||
|
||||
console.log("HOTKEY", e.key);
|
||||
|
||||
for (const [hkAction, hkKeys] of Object.entries(hotkeys) as [HotkeyAction, string[]][]) {
|
||||
for (const hkKey of hkKeys) {
|
||||
const keys = hkKey.split('+');
|
||||
if (
|
||||
keys.length === currentKeys.current.size &&
|
||||
keys.every((key) => currentKeys.current.has(key))
|
||||
) {
|
||||
// Triggered hotkey!
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
callbackRef.current(hkAction, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
clearCurrentKeys();
|
||||
};
|
||||
const up = (e: KeyboardEvent) => {
|
||||
if (options.enable === false) {
|
||||
return;
|
||||
}
|
||||
currentKeys.current.delete(normalizeKey(e.key, os));
|
||||
};
|
||||
window.addEventListener('keydown', down);
|
||||
window.addEventListener('keyup', up);
|
||||
return () => {
|
||||
window.removeEventListener('keydown', down);
|
||||
window.removeEventListener('keyup', up);
|
||||
};
|
||||
}, [options.enable, os]);
|
||||
}
|
||||
|
||||
export function useHotKeyLabel(action: HotkeyAction): string {
|
||||
return hotkeyLabels[action];
|
||||
}
|
||||
|
||||
export function useFormattedHotkey(action: HotkeyAction | null): string[] | null {
|
||||
const osInfo = useOsInfo();
|
||||
const trigger = action != null ? hotkeys[action]?.[0] ?? null : null;
|
||||
if (trigger == null || osInfo == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const os = osInfo.osType;
|
||||
const parts = trigger.split('+');
|
||||
const labelParts: string[] = [];
|
||||
|
||||
for (const p of parts) {
|
||||
if (os === 'Darwin') {
|
||||
if (p === 'CmdCtrl') {
|
||||
labelParts.push('⌘');
|
||||
} else if (p === 'Shift') {
|
||||
labelParts.push('⇧');
|
||||
} else if (p === 'Control') {
|
||||
labelParts.push('⌃');
|
||||
} else if (p === 'Enter') {
|
||||
labelParts.push('↩');
|
||||
} else if (p === 'Tab') {
|
||||
labelParts.push('⇥');
|
||||
} else {
|
||||
labelParts.push(capitalize(p));
|
||||
}
|
||||
} else {
|
||||
if (p === 'CmdCtrl') {
|
||||
labelParts.push('Ctrl');
|
||||
} else {
|
||||
labelParts.push(capitalize(p));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (os === 'Darwin') {
|
||||
return labelParts;
|
||||
} else {
|
||||
return [labelParts.join('+')];
|
||||
}
|
||||
}
|
||||
|
||||
const normalizeKey = (key: string, os: OsType | null) => {
|
||||
if (key === 'Meta' && os === 'Darwin') return 'CmdCtrl';
|
||||
else if (key === 'Control' && os !== 'Darwin') return 'CmdCtrl';
|
||||
else return key;
|
||||
};
|
||||
@@ -13,9 +13,7 @@ export function useResponses(requestId: string | null) {
|
||||
initialData: [],
|
||||
queryKey: responsesQueryKey({ requestId: requestId ?? 'n/a' }),
|
||||
queryFn: async () => {
|
||||
return (await invoke('list_responses', {
|
||||
requestId,
|
||||
})) as HttpResponse[];
|
||||
return (await invoke('list_responses', { requestId, limit: 200 })) as HttpResponse[];
|
||||
},
|
||||
}).data ?? []
|
||||
);
|
||||
|
||||
@@ -10,7 +10,7 @@ export function useSendAnyRequest() {
|
||||
const alert = useAlert();
|
||||
return useMutation<HttpResponse, string, string | null>({
|
||||
mutationFn: (id) => invoke('send_request', { requestId: id, environmentId }),
|
||||
onSettled: () => trackEvent('http_request', 'send'),
|
||||
onSettled: () => trackEvent('HttpRequest', 'Send'),
|
||||
onError: (err) => alert({ title: 'Export Failed', body: err }),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -10,6 +10,5 @@ export function useSendManyRequests() {
|
||||
sendAnyRequest.mutate(id);
|
||||
}
|
||||
},
|
||||
onSettled: () => trackEvent('http_request', 'send'),
|
||||
});
|
||||
}
|
||||
|
||||
18
src-web/hooks/useSettings.ts
Normal file
18
src-web/hooks/useSettings.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import type { Settings } from '../lib/models';
|
||||
|
||||
export function settingsQueryKey() {
|
||||
return ['settings'];
|
||||
}
|
||||
|
||||
export function useSettings() {
|
||||
return (
|
||||
useQuery({
|
||||
queryKey: settingsQueryKey(),
|
||||
queryFn: async () => {
|
||||
return (await invoke('get_settings')) as Settings;
|
||||
},
|
||||
}).data ?? undefined
|
||||
);
|
||||
}
|
||||
@@ -1,30 +1,35 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import type { Appearance } from '../lib/theme/window';
|
||||
import {
|
||||
getAppearance,
|
||||
setAppearance,
|
||||
setAppearanceOnDocument,
|
||||
getPreferredAppearance,
|
||||
subscribeToPreferredAppearanceChange,
|
||||
} from '../lib/theme/window';
|
||||
import { useKeyValue } from './useKeyValue';
|
||||
import { useSettings } from './useSettings';
|
||||
|
||||
export function useTheme() {
|
||||
const appearanceKv = useKeyValue<Appearance>({
|
||||
key: 'appearance',
|
||||
defaultValue: getAppearance(),
|
||||
});
|
||||
const [preferredAppearance, setPreferredAppearance] = useState<Appearance>(
|
||||
getPreferredAppearance(),
|
||||
);
|
||||
|
||||
const handleToggleAppearance = async () => {
|
||||
appearanceKv.set(appearanceKv.value === 'dark' ? 'light' : 'dark');
|
||||
};
|
||||
const settings = useSettings();
|
||||
|
||||
// Set appearance when preferred theme changes
|
||||
useEffect(() => subscribeToPreferredAppearanceChange(appearanceKv.set), [appearanceKv.set]);
|
||||
useEffect(() => {
|
||||
return subscribeToPreferredAppearanceChange(setPreferredAppearance);
|
||||
}, []);
|
||||
|
||||
// Sync appearance when k/v changes
|
||||
useEffect(() => setAppearance(appearanceKv.value), [appearanceKv.value]);
|
||||
const appearance =
|
||||
settings == null || settings?.appearance === 'system'
|
||||
? preferredAppearance
|
||||
: settings.appearance;
|
||||
|
||||
return {
|
||||
appearance: appearanceKv.value,
|
||||
toggleAppearance: handleToggleAppearance,
|
||||
};
|
||||
useEffect(() => {
|
||||
if (settings == null) {
|
||||
return;
|
||||
}
|
||||
setAppearanceOnDocument(settings.appearance as Appearance);
|
||||
}, [appearance, settings]);
|
||||
|
||||
return { appearance };
|
||||
}
|
||||
|
||||
18
src-web/hooks/useUpdateSettings.ts
Normal file
18
src-web/hooks/useUpdateSettings.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import type { HttpRequest, Settings } from '../lib/models';
|
||||
import { requestsQueryKey } from './useRequests';
|
||||
import { settingsQueryKey } from './useSettings';
|
||||
|
||||
export function useUpdateSettings() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<void, unknown, Settings>({
|
||||
mutationFn: async (settings) => {
|
||||
await invoke('update_settings', { settings });
|
||||
},
|
||||
onMutate: async (settings) => {
|
||||
queryClient.setQueryData<Settings>(settingsQueryKey(), settings);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,58 +1,20 @@
|
||||
import { getVersion } from '@tauri-apps/api/app';
|
||||
import type { Environment, Folder, HttpRequest, HttpResponse, KeyValue, Workspace } from './models';
|
||||
|
||||
const appVersion = await getVersion();
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
|
||||
export function trackEvent(
|
||||
resource:
|
||||
| Workspace['model']
|
||||
| Environment['model']
|
||||
| Folder['model']
|
||||
| HttpRequest['model']
|
||||
| HttpResponse['model']
|
||||
| KeyValue['model'],
|
||||
event: 'create' | 'update' | 'delete' | 'delete_many' | 'send' | 'duplicate',
|
||||
| 'App'
|
||||
| 'Workspace'
|
||||
| 'Environment'
|
||||
| 'Folder'
|
||||
| 'HttpRequest'
|
||||
| 'HttpResponse'
|
||||
| 'KeyValue',
|
||||
action: 'Launch' | 'Create' | 'Update' | 'Delete' | 'DeleteMany' | 'Send' | 'Duplicate',
|
||||
attributes: Record<string, string | number> = {},
|
||||
) {
|
||||
send('/e', [
|
||||
{ name: 'e', value: `${resource}.${event}` },
|
||||
{ name: 'a', value: JSON.stringify({ ...attributes, version: appVersion }) },
|
||||
]);
|
||||
}
|
||||
|
||||
export function trackPage(pathname: string) {
|
||||
if (pathname === sessionStorage.lastPathName) {
|
||||
return;
|
||||
}
|
||||
|
||||
sessionStorage.lastPathName = pathname;
|
||||
send('/p', [
|
||||
{
|
||||
name: 'h',
|
||||
value: 'desktop.yaak.app',
|
||||
},
|
||||
{ name: 'p', value: pathname },
|
||||
]);
|
||||
}
|
||||
|
||||
function send(path: string, params: { name: string; value: string | number }[]) {
|
||||
if (localStorage.disableAnalytics === 'true') {
|
||||
console.log('Analytics disabled', path, params);
|
||||
}
|
||||
|
||||
params.push({ name: 'id', value: 'site_zOK0d7jeBy2TLxFCnZ' });
|
||||
params.push({
|
||||
name: 'tz',
|
||||
value: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
});
|
||||
params.push({ name: 'xy', value: screensize() });
|
||||
const qs = params.map((v) => `${v.name}=${encodeURIComponent(v.value)}`).join('&');
|
||||
const url = `https://t.yaak.app/t${path}?${qs}`;
|
||||
fetch(url, { mode: 'no-cors' }).catch((err) => console.log('Error:', err));
|
||||
}
|
||||
|
||||
function screensize() {
|
||||
const w = window.screen.width;
|
||||
const h = window.screen.height;
|
||||
return `${Math.round(w / 100) * 100}x${Math.round(h / 100) * 100}`;
|
||||
invoke('track_event', {
|
||||
resource: resource,
|
||||
action,
|
||||
attributes,
|
||||
}).catch(console.error);
|
||||
}
|
||||
|
||||
3
src-web/lib/capitalize.ts
Normal file
3
src-web/lib/capitalize.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export function capitalize(str: string): string {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
}
|
||||
@@ -7,7 +7,8 @@ export function fallbackRequestName(r: HttpRequest | null): string {
|
||||
return r.name;
|
||||
}
|
||||
|
||||
if (r.url.trim() === '') {
|
||||
const withoutVariables = r.url.replace(/\$\{\[[^\]]+]}/g, '');
|
||||
if (withoutVariables.trim() === '') {
|
||||
return 'New Request';
|
||||
}
|
||||
|
||||
@@ -21,5 +22,5 @@ export function fallbackRequestName(r: HttpRequest | null): string {
|
||||
// Nothing
|
||||
}
|
||||
|
||||
return '';
|
||||
return r.url;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ export const AUTH_TYPE_NONE = null;
|
||||
export const AUTH_TYPE_BASIC = 'basic';
|
||||
export const AUTH_TYPE_BEARER = 'bearer';
|
||||
|
||||
export type Model = Workspace | HttpRequest | HttpResponse | KeyValue | Environment;
|
||||
export type Model = Settings | Workspace | HttpRequest | HttpResponse | KeyValue | Environment;
|
||||
|
||||
export interface BaseModel {
|
||||
readonly id: string;
|
||||
@@ -17,6 +17,15 @@ export interface BaseModel {
|
||||
readonly updatedAt: string;
|
||||
}
|
||||
|
||||
export interface Settings extends BaseModel {
|
||||
readonly model: 'settings';
|
||||
validateCertificates: boolean;
|
||||
followRedirects: boolean;
|
||||
requestTimeout: number;
|
||||
theme: string;
|
||||
appearance: string;
|
||||
}
|
||||
|
||||
export interface Workspace extends BaseModel {
|
||||
readonly model: 'workspace';
|
||||
name: string;
|
||||
@@ -84,7 +93,6 @@ export interface HttpResponse extends BaseModel {
|
||||
readonly workspaceId: string;
|
||||
readonly model: 'http_response';
|
||||
readonly requestId: string;
|
||||
readonly body: number[] | null;
|
||||
readonly bodyPath: string | null;
|
||||
readonly contentLength: number | null;
|
||||
readonly error: string;
|
||||
|
||||
@@ -2,10 +2,6 @@ import { readBinaryFile, readTextFile } from '@tauri-apps/api/fs';
|
||||
import type { HttpResponse } from './models';
|
||||
|
||||
export async function getResponseBodyText(response: HttpResponse): Promise<string | null> {
|
||||
if (response.body) {
|
||||
const uint8Array = Uint8Array.from(response.body);
|
||||
return new TextDecoder().decode(uint8Array);
|
||||
}
|
||||
if (response.bodyPath) {
|
||||
return await readTextFile(response.bodyPath);
|
||||
}
|
||||
@@ -13,9 +9,6 @@ export async function getResponseBodyText(response: HttpResponse): Promise<strin
|
||||
}
|
||||
|
||||
export async function getResponseBodyBlob(response: HttpResponse): Promise<Uint8Array | null> {
|
||||
if (response.body) {
|
||||
return Uint8Array.from(response.body);
|
||||
}
|
||||
if (response.bodyPath) {
|
||||
return readBinaryFile(response.bodyPath);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import type { Environment, Folder, HttpRequest, Workspace } from './models';
|
||||
import type { Environment, Folder, HttpRequest, Settings, Workspace } from './models';
|
||||
|
||||
export async function getSettings(): Promise<Settings> {
|
||||
return invoke('get_settings', {});
|
||||
}
|
||||
|
||||
export async function getRequest(id: string | null): Promise<HttpRequest | null> {
|
||||
if (id === null) return null;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import type { AppTheme, AppThemeColors } from './theme';
|
||||
import { generateCSS, toTailwindVariable } from './theme';
|
||||
|
||||
export type Appearance = 'dark' | 'light';
|
||||
export type Appearance = 'dark' | 'light' | 'system';
|
||||
|
||||
const DEFAULT_APPEARANCE: Appearance = 'system';
|
||||
|
||||
enum Theme {
|
||||
yaak = 'yaak',
|
||||
@@ -61,19 +63,11 @@ const lightTheme: AppTheme = {
|
||||
},
|
||||
};
|
||||
|
||||
export function getAppearance(): Appearance {
|
||||
const docAppearance = document.documentElement.getAttribute('data-appearance');
|
||||
if (docAppearance === 'dark' || docAppearance === 'light') {
|
||||
return docAppearance;
|
||||
}
|
||||
return getPreferredAppearance();
|
||||
}
|
||||
export function setAppearanceOnDocument(appearance: Appearance = DEFAULT_APPEARANCE) {
|
||||
const resolvedAppearance = appearance === 'system' ? getPreferredAppearance() : appearance;
|
||||
const theme = resolvedAppearance === 'dark' ? darkTheme : lightTheme;
|
||||
|
||||
export function setAppearance(a?: Appearance) {
|
||||
const appearance = a ?? getPreferredAppearance();
|
||||
const theme = appearance === 'dark' ? darkTheme : lightTheme;
|
||||
|
||||
document.documentElement.setAttribute('data-appearance', appearance);
|
||||
document.documentElement.setAttribute('data-resolved-appearance', resolvedAppearance);
|
||||
document.documentElement.setAttribute('data-theme', theme.name);
|
||||
|
||||
let existingStyleEl = document.head.querySelector(`style[data-theme-definition]`);
|
||||
@@ -85,11 +79,11 @@ export function setAppearance(a?: Appearance) {
|
||||
|
||||
existingStyleEl.textContent = [
|
||||
`/* ${darkTheme.name} */`,
|
||||
`[data-appearance="dark"] {`,
|
||||
`[data-resolved-appearance="dark"] {`,
|
||||
...generateCSS(darkTheme).map(toTailwindVariable),
|
||||
'}',
|
||||
`/* ${lightTheme.name} */`,
|
||||
`[data-appearance="light"] {`,
|
||||
`[data-resolved-appearance="light"] {`,
|
||||
...generateCSS(lightTheme).map(toTailwindVariable),
|
||||
'}',
|
||||
].join('\n');
|
||||
|
||||
@@ -6,7 +6,11 @@
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
@apply w-full h-full overflow-hidden bg-gray-50 text-gray-900;
|
||||
@apply w-full h-full overflow-hidden;
|
||||
}
|
||||
|
||||
#root {
|
||||
@apply bg-gray-50 text-gray-900;
|
||||
}
|
||||
|
||||
/* Setup default transitions for elements */
|
||||
@@ -68,4 +72,14 @@
|
||||
--color-white: 255 100% 100%;
|
||||
--color-black: 255 0% 0%;
|
||||
}
|
||||
|
||||
select {
|
||||
@apply appearance-none;
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
|
||||
background-position: right 0.5rem center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 1.5em 1.5em;
|
||||
padding-right: 2.5rem;
|
||||
-webkit-print-color-adjust: exact;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,26 +2,31 @@ import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { attachConsole } from 'tauri-plugin-log-api';
|
||||
import { App } from './components/App';
|
||||
import { getKeyValue } from './lib/keyValueStore';
|
||||
import { maybeRestorePathname } from './lib/persistPathname';
|
||||
import { getPreferredAppearance, setAppearance } from './lib/theme/window';
|
||||
import './main.css';
|
||||
import { getSettings } from './lib/store';
|
||||
import type { Appearance } from './lib/theme/window';
|
||||
import { setAppearanceOnDocument } from './lib/theme/window';
|
||||
import { appWindow } from '@tauri-apps/api/window';
|
||||
import { type } from '@tauri-apps/api/os';
|
||||
|
||||
// Hide decorations here because it doesn't work in Rust for some reason (bug?)
|
||||
const osType = await type();
|
||||
if (osType !== 'Darwin') {
|
||||
await appWindow.setDecorations(false);
|
||||
}
|
||||
|
||||
await attachConsole();
|
||||
await maybeRestorePathname();
|
||||
|
||||
const settings = await getSettings();
|
||||
setAppearanceOnDocument(settings.appearance as Appearance);
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
// Don't go back in history on backspace
|
||||
if (e.key === 'Backspace') e.preventDefault();
|
||||
});
|
||||
|
||||
setAppearance(
|
||||
await getKeyValue({
|
||||
key: 'appearance',
|
||||
fallback: getPreferredAppearance(),
|
||||
}),
|
||||
);
|
||||
|
||||
createRoot(document.getElementById('root') as HTMLElement).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
|
||||
@@ -8,7 +8,7 @@ const height = {
|
||||
|
||||
/** @type {import("tailwindcss").Config} */
|
||||
module.exports = {
|
||||
darkMode: ['class', '[data-appearance="dark"]'],
|
||||
darkMode: ['class', '[data-resolved-appearance="dark"]'],
|
||||
content: ['./index.html', './src-web/**/*.{html,js,jsx,ts,tsx}'],
|
||||
theme: {
|
||||
extend: {
|
||||
|
||||
Reference in New Issue
Block a user