Compare commits

..

29 Commits

Author SHA1 Message Date
Gregory Schier
626aacf982 Bump version to 2024.0.0 2024-01-08 15:57:59 -08:00
Gregory Schier
d5855c45a6 Hotkey labels 2024-01-08 15:57:21 -08:00
Gregory Schier
793bff9f27 Show hotkeys on empty views 2024-01-08 15:13:44 -08:00
Gregory Schier
88ea68e72f Remove base env, fix hotkeys, and QoL improvements 2024-01-07 22:24:19 -08:00
Gregory Schier
35e40d2c55 Fix hotkeys getting stuck on cmd+tab 2024-01-07 21:32:25 -08:00
Gregory Schier
c472b83409 Always show settings dropdown 2023-11-22 09:39:30 -08:00
Gregory Schier
52c26d235c Tweak margin 2023-11-22 09:37:50 -08:00
Gregory Schier
ac54729012 Fix bottom-up dropdown positioning 2023-11-22 09:35:56 -08:00
Gregory Schier
0586034ef4 Bump version 2023-11-22 09:06:47 -08:00
Gregory Schier
91790ba708 Better linux/Windows support for hotkeys 2023-11-22 09:06:22 -08:00
Gregory Schier
d8ab6c0b50 Good hotkey support 2023-11-22 09:01:48 -08:00
Gregory Schier
b600a21a2b Reset URL bar when request changes 2023-11-21 23:26:29 -08:00
Gregory Schier
4f9d1278f7 Env dialog hotkey 2023-11-21 22:35:28 -08:00
Gregory Schier
15aa93f5f9 Remove response body and basic hotkeys 2023-11-21 22:15:01 -08:00
Gregory Schier
c7798092d8 Remove app-specific menu items 2023-11-21 19:18:40 -08:00
Gregory Schier
5560593aaa Fix macOS menu and fallback URL 2023-11-21 09:24:13 -08:00
Gregory Schier
66639e651d Hide menu on windows/linux 2023-11-21 08:17:37 -08:00
Gregory Schier
8e42d5ccdb Disable sandboxing (again) 2023-11-19 21:59:55 -08:00
Gregory Schier
5c62594087 Fix drag-drop reorder 2023-11-19 21:43:01 -08:00
Gregory Schier
26b6c48657 Postman ID generation 2023-11-19 20:54:02 -08:00
Gregory Schier
0290aba982 Bump beta.3 2023-11-19 20:46:55 -08:00
Gregory Schier
0bafc4e4f5 Postman variables + urlencoded forms 2023-11-19 20:29:24 -08:00
Gregory Schier
9a36f94279 Add back Windows/Linux builds 2023-11-19 18:22:13 -08:00
Gregory Schier
1d8e66179e Remove Tauri context menu plugin 2023-11-19 18:21:10 -08:00
Gregory Schier
fda6d16d8e Fix header padding windows/linux 2023-11-19 18:14:49 -08:00
Gregory Schier
c4737916df Some tweaks 2023-11-19 18:13:32 -08:00
Gregory Schier
919465cdbb Beta 2 2023-11-19 17:41:58 -08:00
Gregory Schier
de3730fa4f Network entitlement 2023-11-19 17:41:46 -08:00
Gregory Schier
aff26fdd46 Try sandboxing again 2023-11-19 17:06:31 -08:00
60 changed files with 2778 additions and 6812 deletions

View File

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

View File

@@ -1,3 +1,4 @@
node_modules/
dist/
out/
.prettierrc.cjs

View File

@@ -6,7 +6,8 @@
<script value="start" />
</scripts>
<node-interpreter value="project" />
<envs />
<envs>
</envs>
<method v="2" />
</configuration>
</component>
</component>

1568
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,7 @@
"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",
@@ -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",

View File

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

View File

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

View 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"
}

View File

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

View File

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

View File

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

165
src-tauri/Cargo.lock generated
View File

@@ -171,12 +171,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 +711,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 +730,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"
@@ -1062,22 +1026,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 +1413,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 +1598,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 +1985,8 @@ dependencies = [
"bytemuck",
"byteorder",
"color_quant",
"exr",
"gif",
"jpeg-decoder",
"num-rational",
"num-traits",
"png",
"qoi",
"tiff",
]
[[package]]
@@ -2196,15 +2119,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 +2174,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"
@@ -3229,15 +3137,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 +3242,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"
@@ -4579,23 +4458,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"
@@ -4773,17 +4635,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 +5281,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"
@@ -5887,7 +5732,6 @@ dependencies = [
"sqlx",
"tauri",
"tauri-build",
"tauri-plugin-context-menu",
"tauri-plugin-log",
"tauri-plugin-window-state",
"tokio",
@@ -5998,12 +5842,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",
]

View File

@@ -47,7 +47,6 @@ tauri-plugin-log = { git = "https://github.com/tauri-apps/plugins-workspace", br
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"
[features]

View File

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

View File

@@ -0,0 +1 @@
ALTER TABLE http_responses DROP COLUMN body;

View File

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

View File

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

View File

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

View File

@@ -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"
]
}

View File

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

View File

@@ -13,14 +13,14 @@ 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 sqlx::migrate::Migrator;
use sqlx::types::Json;
use tauri::{AppHandle, Menu, RunEvent, State, Submenu, Window, WindowUrl, Wry};
use tauri::{CustomMenuItem, Manager, WindowEvent};
use tauri::{AppHandle, RunEvent, State, Window, WindowUrl, Wry};
use tauri::{Manager, WindowEvent};
#[cfg(target_os = "macos")]
use tauri::TitleBarStyle;
use tauri_plugin_log::{fern, LogTarget};
@@ -186,7 +186,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");
@@ -551,10 +551,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,7 +658,6 @@ fn main() {
.build(),
)
.plugin(tauri_plugin_window_state::Builder::default().build())
.plugin(tauri_plugin_context_menu::init())
.setup(|app| {
let dir = match is_dev() {
true => current_dir().unwrap(),
@@ -666,6 +666,12 @@ fn main() {
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);
@@ -747,7 +753,7 @@ fn main() {
debug!("Updater downloaded");
}
tauri::UpdaterEvent::Error(e) => {
error!("Updater error: {:?}", e);
warn!("Updater received error: {:?}", e);
}
_ => {}
},
@@ -784,28 +790,16 @@ 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 +807,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,6 +821,7 @@ 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);
}
@@ -900,8 +894,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,

View File

@@ -124,7 +124,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>>,
}
@@ -594,7 +593,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 +611,10 @@ pub async fn create_response(
status,
status_reason,
content_length,
body,
body_path,
headers
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
"#,
id,
request_id,
@@ -627,7 +624,6 @@ pub async fn create_response(
status,
status_reason,
content_length,
body,
body_path,
headers_json,
)
@@ -704,19 +700,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 +726,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 +739,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 +772,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 +811,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(())

View File

@@ -228,11 +228,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

View File

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

View File

@@ -8,7 +8,7 @@
},
"package": {
"productName": "Yaak",
"version": "2023.3.0"
"version": "2024.0.0"
},
"tauri": {
"windows": [],
@@ -56,7 +56,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/*",

View File

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

View File

@@ -2,7 +2,9 @@ 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 { useHotkey } from '../hooks/useHotkey';
import type { ButtonProps } from './core/Button';
import { Button } from './core/Button';
import type { DropdownItem } from './core/Dropdown';
@@ -21,16 +23,20 @@ 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} />,
});
}, [dialog, activeEnvironment]);
useHotkey('environmentEditor.toggle', showEnvironmentDialog, { enable: environments.length > 0 });
const items: DropdownItem[] = useMemo(
() => [
...environments.map(
@@ -51,14 +57,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 (

View File

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

View File

@@ -92,9 +92,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');
}
});

View File

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

View File

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

View File

@@ -3,8 +3,12 @@ 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 { useCreateRequest } from '../hooks/useCreateRequest';
import { useRequests } from '../hooks/useRequests';
import { clamp } from '../lib/clamp';
import { HotKeyList } from './core/HotKeyList';
import { RequestPane } from './RequestPane';
import { ResizeHandle } from './ResizeHandle';
import { ResponsePane } from './ResponsePane';
@@ -24,6 +28,9 @@ const STACK_VERTICAL_WIDTH = 600;
export const RequestResponse = memo(function RequestResponse({ style }: Props) {
const containerRef = useRef<HTMLDivElement>(null);
const activeRequest = useActiveRequest();
const createRequest = useCreateRequest();
const requests = useRequests();
const [vertical, setVertical] = useState<boolean>(false);
const [widthRaw, setWidth] = useLocalStorage<number>(`body_width::${useActiveWorkspaceId()}`);
const [heightRaw, setHeight] = useLocalStorage<number>(`body_height::${useActiveWorkspaceId()}`);
@@ -114,6 +121,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} />

View File

@@ -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} />
) : (

View File

@@ -0,0 +1,93 @@
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 { useTheme } from '../hooks/useTheme';
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';
export function SettingsDropdown() {
const importData = useImportData();
const exportData = useExportData();
const { appearance, toggleAppearance } = useTheme();
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: 'appearance',
label: 'Toggle Theme',
onSelect: toggleAppearance,
leftSlot: <Icon icon={appearance === 'dark' ? 'sun' : 'moon'} />,
},
{ type: 'separator', label: `v${appVersion.data}` },
{
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" />,
},
{
key: 'feedback',
label: 'Feedback',
onSelect: () => shell.open('https://yaak.canny.io'),
leftSlot: <Icon icon="chat" />,
},
]}
>
<IconButton size="sm" title="Request Options" icon="gear" className="pointer-events-auto" />
</Dropdown>
);
}

View File

@@ -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,9 +53,9 @@ interface TreeNode {
export function Sidebar({ className }: Props) {
const { hidden } = useSidebarHidden();
const createRequest = useCreateRequest();
const sidebarRef = useRef<HTMLLIElement>(null);
const activeRequestId = useActiveRequestId();
const duplicateRequest = useDuplicateRequest({ id: activeRequestId ?? '', navigateAfter: true });
const activeEnvironmentId = useActiveEnvironmentId();
const requests = useRequests();
const folders = useFolders();
@@ -78,6 +76,8 @@ export function Sidebar({ className }: Props) {
namespace: NAMESPACE_NO_SYNC,
});
useHotkey('request.duplicate', () => duplicateRequest.mutate());
const isCollapsed = useCallback(
(id: string) => collapsed.value?.[id] ?? false,
[collapsed.value],
@@ -86,10 +86,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 +127,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 +209,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 +319,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 +522,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 +575,96 @@ 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: () => {
console.log('DUPLICATE');
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 +759,13 @@ function DraggableSidebarItem({
[onMove],
);
const [{ isDragging }, connectDrag] = useDrag<DragItem, unknown, { isDragging: boolean }>(
const [{ isDragging }, connectDrag] = useDrag<
DragItem,
unknown,
{
isDragging: boolean;
}
>(
() => ({
type: ItemTypes.REQUEST,
item: () => {

View File

@@ -1,9 +1,9 @@
import { memo } from 'react';
import { useCreateFolder } from '../hooks/useCreateFolder';
import { useCreateRequest } from '../hooks/useCreateRequest';
import { useHotkey } from '../hooks/useHotkey';
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';
@@ -12,6 +12,8 @@ export const SidebarActions = memo(function SidebarActions() {
const createFolder = useCreateFolder();
const { hidden, toggle } = useSidebarHidden();
useHotkey('request.create', () => createRequest.mutate({}));
return (
<HStack>
<IconButton
@@ -19,6 +21,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 +29,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({}),
},
]}

View File

@@ -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"
/>
}
/>

View File

@@ -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,7 +177,7 @@ function HeaderSize({ className, ...props }: HeaderSizeProps) {
<div
className={classNames(
className,
'h-md pt-[1px] flex items-center w-full pr-3 pl-20 border-b',
'h-md pt-[1px] flex items-center w-full pr-3 border-b',
platform?.osType === 'Darwin' && 'pl-20',
)}
{...props}

View File

@@ -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,
]);

View File

@@ -1,13 +1,10 @@
import classNames from 'classnames';
import React, { memo } from 'react';
import { useActiveRequest } from '../hooks/useActiveRequest';
import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
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';
@@ -16,9 +13,6 @@ interface Props {
}
export const WorkspaceHeader = memo(function WorkspaceHeader({ className }: Props) {
const activeRequest = useActiveRequest();
const activeWorkspace = useActiveWorkspace();
return (
<HStack
space={2}
@@ -29,13 +23,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>
@@ -44,14 +32,7 @@ export const WorkspaceHeader = memo(function WorkspaceHeader({ className }: Prop
<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>
<SettingsDropdown />
</div>
</HStack>
);

View File

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

View File

@@ -20,8 +20,10 @@ import React, {
useState,
} from 'react';
import { useKey, useKeyPressEvent, useWindowSize } from 'react-use';
import type { HotkeyAction } from '../../hooks/useHotkey';
import { Overlay } from '../Overlay';
import { Button } from './Button';
import { HotKey } from './HotKey';
import { Separator } from './Separator';
import { VStack } from './Stacks';
@@ -30,19 +32,20 @@ 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>>;
@@ -126,9 +129,10 @@ export const Dropdown = forwardRef<DropdownRef, DropdownProps>(function Dropdown
{open && triggerRect && (
<Menu
ref={menuRef}
showTriangle
defaultSelectedIndex={defaultSelectedIndex}
items={items}
triggerRect={triggerRect}
triggerShape={triggerRect}
onClose={handleClose}
/>
)}
@@ -136,16 +140,53 @@ export const Dropdown = forwardRef<DropdownRef, DropdownProps>(function Dropdown
);
});
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],
);
if (show === null) {
return null;
}
return (
<Menu
className={className}
ref={ref}
items={items}
onClose={onClose}
triggerShape={triggerShape}
/>
);
});
interface MenuProps {
className?: string;
defaultSelectedIndex?: number;
items: DropdownProps['items'];
triggerRect: DOMRect;
triggerShape: Pick<DOMRect, 'top' | 'bottom' | 'left' | 'right'>;
onClose: () => void;
showTriangle?: boolean;
}
const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle'>, MenuProps>(function Menu(
{ className, items, onClose, triggerRect, defaultSelectedIndex }: MenuProps,
{ className, items, onClose, triggerShape, defaultSelectedIndex, showTriangle }: MenuProps,
ref,
) {
const containerRef = useRef<HTMLDivElement | null>(null);
@@ -248,21 +289,27 @@ 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 };
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) => {
@@ -288,13 +335,15 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle'>, MenuPro
dir="ltr"
ref={containerRef}
style={containerStyles}
className={classNames(className, 'outline-none mt-1 pointer-events-auto fixed z-50')}
className={classNames(className, 'outline-none my-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"
/>
{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}
@@ -333,9 +382,9 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle'>, MenuPro
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 +408,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 +422,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',

View File

@@ -1,21 +1,30 @@
import classNames from 'classnames';
import type { HotkeyAction } from '../../hooks/useHotkey';
import { useFormattedHotkey } from '../../hooks/useHotkey';
import { useOsInfo } from '../../hooks/useOsInfo';
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 label = useFormattedHotkey(action);
if (label === null || osInfo == null) {
return null;
}
export function HotKey({ modifier, keyName }: Props) {
return (
<span className={classNames('text-sm text-gray-600')}>
{keys[modifier]}
{keyName}
<span
className={classNames(
className,
variant === 'with-bg' && 'rounded border',
'text-sm text-gray-1000 text-opacity-disabled',
)}
>
{label}
</span>
);
}

View File

@@ -0,0 +1,11 @@
import type { HotkeyAction } from '../../hooks/useHotkey';
import { useHotKeyLabel } from '../../hooks/useHotkey';
interface Props {
action: HotkeyAction | null;
}
export function HotKeyLabel({ action }: Props) {
const label = useHotKeyLabel(action);
return <span>{label}</span>;
}

View File

@@ -0,0 +1,23 @@
import React from 'react';
import type { HotkeyAction } from '../../hooks/useHotkey';
import { HotKey } from './HotKey';
import { HotKeyLabel } from './HotKeyLabel';
interface Props {
hotkeys: HotkeyAction[];
}
export const HotKeyList = ({ hotkeys }: Props) => {
return (
<div className="mx-auto h-full flex items-center text-gray-700 text-sm">
<div className="flex flex-col gap-1">
{hotkeys.map((hotkey) => (
<div key={hotkey} className="grid grid-cols-2">
<HotKeyLabel action={hotkey} />
<HotKey className="ml-auto" action={hotkey} />
</div>
))}
</div>
</div>
);
};

View File

@@ -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 ReactIcons from '@radix-ui/react-icons';
import classNames from 'classnames';
import type { HTMLAttributes } from 'react';
import { memo } from 'react';
@@ -46,47 +6,50 @@ 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: ReactIcons.ArchiveIcon,
camera: ReactIcons.CameraIcon,
chat: ReactIcons.ChatBubbleIcon,
check: ReactIcons.CheckIcon,
checkbox: ReactIcons.CheckboxIcon,
clock: ReactIcons.ClockIcon,
chevronDown: ReactIcons.ChevronDownIcon,
chevronRight: ReactIcons.ChevronRightIcon,
code: ReactIcons.CodeIcon,
colorWheel: ReactIcons.ColorWheelIcon,
copy: ReactIcons.CopyIcon,
dividerH: ReactIcons.DividerHorizontalIcon,
dotsH: ReactIcons.DotsHorizontalIcon,
dotsV: ReactIcons.DotsVerticalIcon,
download: ReactIcons.DownloadIcon,
drag: ReactIcons.DragHandleDots2Icon,
eye: ReactIcons.EyeOpenIcon,
eyeClosed: ReactIcons.EyeClosedIcon,
gear: ReactIcons.GearIcon,
hamburger: ReactIcons.HamburgerMenuIcon,
home: ReactIcons.HomeIcon,
listBullet: ReactIcons.ListBulletIcon,
magicWand: ReactIcons.MagicWandIcon,
magnifyingGlass: ReactIcons.MagnifyingGlassIcon,
moon: ReactIcons.MoonIcon,
openNewWindow: ReactIcons.OpenInNewWindowIcon,
paperPlane: ReactIcons.PaperPlaneIcon,
pencil: ReactIcons.Pencil2Icon,
plus: ReactIcons.PlusIcon,
plusCircle: ReactIcons.PlusCircledIcon,
question: ReactIcons.QuestionMarkIcon,
rows: ReactIcons.RowsIcon,
sun: ReactIcons.SunIcon,
trash: ReactIcons.TrashIcon,
triangleDown: ReactIcons.TriangleDownIcon,
triangleLeft: ReactIcons.TriangleLeftIcon,
triangleRight: ReactIcons.TriangleRightIcon,
update: ReactIcons.UpdateIcon,
upload: ReactIcons.UploadIcon,
x: ReactIcons.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} />,
};

View File

@@ -38,6 +38,7 @@ export const IconButton = forwardRef<HTMLButtonElement, Props>(function IconButt
},
[onClick, setConfirmed, showConfirm],
);
return (
<Button
ref={ref}

View File

@@ -426,7 +426,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>
);

View File

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

View File

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

View File

@@ -24,11 +24,7 @@ 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'),
onSuccess: async (environment) => {

View File

@@ -15,7 +15,7 @@ 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'),

View File

@@ -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,7 +24,8 @@ 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'),
@@ -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));
}

View File

@@ -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 });
},
});
}

161
src-web/hooks/useHotkey.ts Normal file
View File

@@ -0,0 +1,161 @@
import type { OsType } from '@tauri-apps/api/os';
import { useEffect, useRef } from 'react';
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';
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'],
};
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));
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 | null): string {
switch (action) {
case 'request.send':
return 'Send Request';
case 'request.create':
return 'New Request';
case 'request.duplicate':
return 'Duplicate Request';
case 'sidebar.toggle':
return 'Toggle Sidebar';
case 'sidebar.focus':
return 'Focus Sidebar';
case 'urlBar.focus':
return 'Focus URL';
case 'environmentEditor.toggle':
return 'Edit Environments';
default:
return 'Unknown';
}
}
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 {
labelParts.push(p.toUpperCase());
}
} else {
if (p === 'CmdCtrl') {
labelParts.push('Ctrl');
} else {
labelParts.push(p);
}
}
}
if (os === 'Darwin') {
return labelParts.join('');
} 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;
};

View File

@@ -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 ?? []
);

View File

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

View File

@@ -84,7 +84,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;

View File

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