diff --git a/.gitignore b/.gitignore index 4c0076a1..358ff797 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,4 @@ dist-ssr .eslintcache *.sqlite +*.sqlite-* diff --git a/package-lock.json b/package-lock.json index d2947855..fde619cc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,6 +41,8 @@ "react-helmet-async": "^1.3.0", "react-router-dom": "^6.8.1", "react-use": "^17.4.0", + "tauri-plugin-context-menu": "^0.5.0", + "tauri-plugin-log-api": "github:tauri-apps/tauri-plugin-log#v1", "uuid": "^9.0.0" }, "devDependencies": { @@ -7475,6 +7477,22 @@ "postcss": "^8.2.14" } }, + "node_modules/tauri-plugin-context-menu": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/tauri-plugin-context-menu/-/tauri-plugin-context-menu-0.5.0.tgz", + "integrity": "sha512-CkAjSyxLx26hTXabG5Unbv+GWeH0XYNQB3zTqRxHpp257mWX8I4oASp8YF5T3zxFQEK++ZHqMZHpLrQ3usShRQ==", + "dependencies": { + "@tauri-apps/api": "^1.5.0" + } + }, + "node_modules/tauri-plugin-log-api": { + "version": "0.0.0", + "resolved": "git+ssh://git@github.com/tauri-apps/tauri-plugin-log.git#e5266f6719039c32b8f51ae86c9b726c2c9f3e42", + "license": "MIT or APACHE-2.0", + "dependencies": { + "@tauri-apps/api": "1.5.1" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", diff --git a/package.json b/package.json index 6eba58cd..a35cc32e 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,8 @@ "react-helmet-async": "^1.3.0", "react-router-dom": "^6.8.1", "react-use": "^17.4.0", + "tauri-plugin-context-menu": "^0.5.0", + "tauri-plugin-log-api": "github:tauri-apps/tauri-plugin-log#v1", "uuid": "^9.0.0" }, "devDependencies": { diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index a8fc04f8..279ab1ec 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -186,6 +186,12 @@ 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" @@ -399,6 +405,16 @@ version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" +[[package]] +name = "byte-unit" +version = "4.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da78b32057b8fdfc352504708feeba7216dcd65a2c9ab02978cbd288d1279b6c" +dependencies = [ + "serde", + "utf8-width", +] + [[package]] name = "bytemuck" version = "1.14.0" @@ -720,6 +736,30 @@ 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" @@ -739,6 +779,12 @@ 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" @@ -997,6 +1043,22 @@ 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 0.11.0", + "half", + "lebe", + "miniz_oxide", + "rayon-core", + "smallvec", + "zune-inflate", +] + [[package]] name = "fast-float" version = "0.2.0" @@ -1018,6 +1080,15 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "fern" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f0c14694cbd524c8720dd69b0e3179344f04ebb5f90f2e4a440c6ea3b2f1ee" +dependencies = [ + "log", +] + [[package]] name = "field-offset" version = "0.3.6" @@ -1068,6 +1139,15 @@ dependencies = [ "spin 0.9.8", ] +[[package]] +name = "flume" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" +dependencies = [ + "spin 0.9.8", +] + [[package]] name = "fnv" version = "1.0.7" @@ -1375,6 +1455,16 @@ 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" @@ -1560,6 +1650,15 @@ 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" @@ -1929,8 +2028,14 @@ dependencies = [ "bytemuck", "byteorder", "color_quant", + "exr", + "gif", + "jpeg-decoder", "num-rational", "num-traits", + "png", + "qoi", + "tiff", ] [[package]] @@ -2043,6 +2148,15 @@ 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.64" @@ -2095,6 +2209,12 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "lebe" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" + [[package]] name = "libappindicator" version = "0.7.1" @@ -2182,6 +2302,9 @@ name = "log" version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +dependencies = [ + "value-bag", +] [[package]] name = "loom" @@ -2482,6 +2605,15 @@ dependencies = [ "syn 2.0.38", ] +[[package]] +name = "num_threads" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" +dependencies = [ + "libc", +] + [[package]] name = "objc" version = "0.2.7" @@ -3007,6 +3139,15 @@ 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.30.0" @@ -3112,6 +3253,26 @@ 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.2.16" @@ -3746,7 +3907,7 @@ dependencies = [ "dotenvy", "either", "event-listener", - "flume", + "flume 0.10.14", "futures-channel", "futures-core", "futures-executor", @@ -4169,10 +4330,42 @@ 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" +source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#61e862597e7234ddca9d9c4b07700855841888f3" +dependencies = [ + "byte-unit", + "fern", + "log", + "serde", + "serde_json", + "serde_repr", + "tauri", + "time", +] + [[package]] name = "tauri-plugin-window-state" version = "0.1.0" -source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#68d77f999c72fd260b86ff57f8fd64755de04361" +source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#61e862597e7234ddca9d9c4b07700855841888f3" dependencies = [ "bincode", "bitflags 2.4.1", @@ -4346,6 +4539,17 @@ 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" @@ -4354,6 +4558,8 @@ checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5" dependencies = [ "deranged", "itoa 1.0.9", + "libc", + "num_threads", "powerfmt", "serde", "time-core", @@ -4694,6 +4900,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52df8b7fb78e7910d776fccf2e42ceaf3604d55e8e7eb2dbd183cb1441d8a692" +[[package]] +name = "utf8-width" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5190c9442dcdaf0ddd50f37420417d219ae5261bbf5db120d0f9bab996c9cba1" + [[package]] name = "utf8_iter" version = "1.0.3" @@ -4715,6 +4927,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +[[package]] +name = "value-bag" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a72e1902dde2bd6441347de2b70b7f5d59bf157c6c62f0c44572607a1d55bbe" + [[package]] name = "vcpkg" version = "0.2.15" @@ -4983,6 +5201,12 @@ dependencies = [ "windows-metadata", ] +[[package]] +name = "weezl" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb" + [[package]] name = "winapi" version = "0.3.9" @@ -5418,6 +5642,7 @@ dependencies = [ "cocoa 0.25.0", "futures", "http", + "log", "objc", "rand 0.8.5", "reqwest", @@ -5426,6 +5651,8 @@ dependencies = [ "sqlx", "tauri", "tauri-build", + "tauri-plugin-context-menu", + "tauri-plugin-log", "tauri-plugin-window-state", "tokio", "uuid", @@ -5529,3 +5756,12 @@ 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", +] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 5fdfca15..d1527f17 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -43,8 +43,11 @@ tauri = { version = "1.3", features = [ "dialog-open", ] } tauri-plugin-window-state = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" } +tauri-plugin-log = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" } tokio = { version = "1.25.0", features = ["sync"] } uuid = "1.3.0" +log = "0.4.20" +tauri-plugin-context-menu = "0.5.0" [features] # by default Tauri runs in production mode diff --git a/src-tauri/plugins/insomnia-importer/out/index.js b/src-tauri/plugins/insomnia-importer/out/index.js index 0fd06c17..0dcb068c 100644 --- a/src-tauri/plugins/insomnia-importer/out/index.js +++ b/src-tauri/plugins/insomnia-importer/out/index.js @@ -17,18 +17,18 @@ function O(e, t) { ); } function g(e) { - return d(e) && e._type === 'workspace'; + return m(e) && e._type === 'workspace'; } function y(e) { - return d(e) && e._type === 'request_group'; + return m(e) && e._type === 'request_group'; } function _(e) { - return d(e) && e._type === 'request'; + return m(e) && e._type === 'request'; } function I(e) { - return d(e) && e._type === 'environment'; + return m(e) && e._type === 'environment'; } -function d(e) { +function m(e) { return Object.prototype.toString.call(e) === '[object Object]'; } function h(e) { @@ -41,31 +41,31 @@ function N(e) { value: `${i}`, })); } -function c(e) { +function p(e) { return h(e) ? e.replaceAll(/{{\s*(_\.)?([^}]+)\s*}}/g, '${[$2]}') : e; } function D(e, t, i = 0) { - var a, u; + var a, d; console.log('IMPORTING REQUEST', e._id, e.name, JSON.stringify(e, null, 2)); let s = null, n = null; ((a = e.body) == null ? void 0 : a.mimeType) === 'application/graphql' - ? ((s = 'graphql'), (n = c(e.body.text))) - : ((u = e.body) == null ? void 0 : u.mimeType) === 'application/json' && - ((s = 'application/json'), (n = c(e.body.text))); - let p = null, - o = {}; + ? ((s = 'graphql'), (n = p(e.body.text))) + : ((d = e.body) == null ? void 0 : d.mimeType) === 'application/json' && + ((s = 'application/json'), (n = p(e.body.text))); + let u = null, + r = {}; return ( e.authentication.type === 'bearer' - ? ((p = 'bearer'), - (o = { - token: c(e.authentication.token), + ? ((u = 'bearer'), + (r = { + token: p(e.authentication.token), })) : e.authentication.type === 'basic' && - ((p = 'basic'), - (o = { - username: c(e.authentication.username), - password: c(e.authentication.password), + ((u = 'basic'), + (r = { + username: p(e.authentication.username), + password: p(e.authentication.password), })), { id: e._id, @@ -76,17 +76,19 @@ function D(e, t, i = 0) { model: 'http_request', sortPriority: i, name: e.name, - url: c(e.url), + url: p(e.url), body: n, bodyType: s, - authentication: o, - authenticationType: p, + authentication: r, + authenticationType: u, method: e.method, - headers: (e.headers ?? []).map(({ name: m, value: r, disabled: f }) => ({ - enabled: !f, - name: m, - value: r, - })), + headers: (e.headers ?? []) + .map(({ name: c, value: o, disabled: f }) => ({ + enabled: !f, + name: c, + value: o, + })) + .filter(({ name: c, value: o }) => c !== '' || o !== ''), } ); } @@ -119,7 +121,7 @@ function b(e, t) { } function T(e) { const t = JSON.parse(e); - if (!d(t)) return; + if (!m(t)) return; const { _type: i, __export_format: s } = t; if (i !== 'export' || s !== 4 || !Array.isArray(t.resources)) return; const n = { @@ -128,23 +130,23 @@ function T(e) { environments: [], folders: [], }, - p = t.resources.filter(g); - for (const o of p) { - console.log('IMPORTING WORKSPACE', o.name); - const a = t.resources.find((r) => I(r) && r.parentId === o._id); + u = t.resources.filter(g); + for (const r of u) { + console.log('IMPORTING WORKSPACE', r.name); + const a = t.resources.find((o) => I(o) && o.parentId === r._id); console.log('FOUND BASE ENV', a.name), - n.workspaces.push(w(o, a ? N(a.data) : [])), + n.workspaces.push(w(r, a ? N(a.data) : [])), console.log('IMPORTING ENVIRONMENTS', a.name); - const u = t.resources.filter((r) => I(r) && r.parentId === (a == null ? void 0 : a._id)); - console.log('FOUND', u.length, 'ENVIRONMENTS'), - n.environments.push(...u.map((r) => O(r, o._id))); - const m = (r) => { - const f = t.resources.filter((l) => l.parentId === r); + const d = t.resources.filter((o) => I(o) && o.parentId === (a == null ? void 0 : a._id)); + console.log('FOUND', d.length, 'ENVIRONMENTS'), + n.environments.push(...d.map((o) => O(o, r._id))); + const c = (o) => { + const f = t.resources.filter((l) => l.parentId === o); let S = 0; for (const l of f) - y(l) ? (n.folders.push(b(l, o._id)), m(l._id)) : _(l) && n.requests.push(D(l, o._id, S++)); + y(l) ? (n.folders.push(b(l, r._id)), c(l._id)) : _(l) && n.requests.push(D(l, r._id, S++)); }; - m(o._id); + c(r._id); } return ( (n.requests = n.requests.filter(Boolean)), diff --git a/src-tauri/plugins/insomnia-importer/src/importers/request.js b/src-tauri/plugins/insomnia-importer/src/importers/request.js index edb06963..9535344b 100644 --- a/src-tauri/plugins/insomnia-importer/src/importers/request.js +++ b/src-tauri/plugins/insomnia-importer/src/importers/request.js @@ -49,10 +49,12 @@ export function importRequest(r, workspaceId, sortPriority = 0) { authentication, authenticationType, method: r.method, - headers: (r.headers ?? []).map(({ name, value, disabled }) => ({ - enabled: !disabled, - name, - value, - })), + headers: (r.headers ?? []) + .map(({ name, value, disabled }) => ({ + enabled: !disabled, + name, + value, + })) + .filter(({ name, value }) => name !== '' || value !== ''), }; } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 4183a2cb..6c183be6 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -11,6 +11,7 @@ use std::collections::HashMap; use std::env::current_dir; use std::fs::{create_dir_all, File}; use std::io::Write; +use std::path::Path; use std::process::exit; use base64::Engine; @@ -20,13 +21,14 @@ use rand::random; use reqwest::redirect::Policy; use serde::Serialize; use sqlx::migrate::Migrator; -use sqlx::sqlite::SqlitePoolOptions; +use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions}; use sqlx::types::Json; -use sqlx::{Pool, Sqlite}; +use sqlx::{ConnectOptions, Pool, Sqlite}; #[cfg(target_os = "macos")] use tauri::TitleBarStyle; use tauri::{AppHandle, Menu, RunEvent, State, Submenu, Window, WindowUrl, Wry}; use tauri::{CustomMenuItem, Manager, WindowEvent}; +use tauri_plugin_log::LogTarget; use tauri_plugin_window_state::{StateFlags, WindowExt}; use tokio::sync::Mutex; @@ -258,6 +260,7 @@ async fn actually_send_request( Err(e) => response_err(response, e.to_string(), app_handle, pool).await, } } + #[tauri::command] async fn import_data( window: Window, @@ -275,6 +278,23 @@ async fn import_data( Ok(imported) } +#[tauri::command] +async fn export_data( + db_instance: State<'_, Mutex>>, + root_dir: &str, + workspace_id: &str, +) -> Result<(), String> { + let path = Path::new(root_dir).join("yaak-export.json"); + let pool = &*db_instance.lock().await; + let imported = models::get_workspace_export_resources(pool, workspace_id).await; + println!("Exporting {:?}", path); + let f = File::create(path).expect("Unable to create file"); + serde_json::to_writer_pretty(f, &imported) + .map_err(|e| e.to_string()) + .expect("Failed to write"); + Ok(()) +} + #[tauri::command] async fn send_request( window: Window, @@ -715,7 +735,13 @@ async fn delete_workspace( fn main() { tauri::Builder::default() + .plugin( + tauri_plugin_log::Builder::default() + .targets([LogTarget::LogDir, LogTarget::Stdout, LogTarget::Webview]) + .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(), @@ -730,7 +756,13 @@ fn main() { tauri::async_runtime::block_on(async move { let pool = SqlitePoolOptions::new() - .connect(url.as_str()) + .connect_with( + SqliteConnectOptions::new() + .filename(p) + .create_if_missing(true) + .disable_statement_logging() + .clone(), + ) .await .expect("Failed to connect to database"); @@ -789,6 +821,7 @@ fn main() { delete_response, delete_workspace, duplicate_request, + export_data, get_key_value, get_environment, get_folder, diff --git a/src-tauri/src/models.rs b/src-tauri/src/models.rs index fc343142..ff317b5c 100644 --- a/src-tauri/src/models.rs +++ b/src-tauri/src/models.rs @@ -794,3 +794,32 @@ pub fn generate_id(prefix: Option<&str>) -> String { Some(p) => format!("{p}_{id}"), }; } + +#[derive(Default, Debug, Deserialize, Serialize)] +pub struct WorkspaceExportResources { + workspaces: Vec, + environments: Vec, + folders: Vec, + requests: Vec, +} + +pub(crate) async fn get_workspace_export_resources( + pool: &Pool, + workspace_id: &str, +) -> WorkspaceExportResources { + let workspace = get_workspace(workspace_id, pool) + .await + .expect("Failed to get workspace"); + return WorkspaceExportResources { + workspaces: vec![workspace], + environments: find_environments(workspace_id, pool) + .await + .expect("Failed to get environments"), + folders: find_folders(workspace_id, pool) + .await + .expect("Failed to get folders"), + requests: find_requests(workspace_id, pool) + .await + .expect("Failed to get requests"), + }; +} diff --git a/src-tauri/src/plugin.rs b/src-tauri/src/plugin.rs index 4d83fa20..90c2b343 100644 --- a/src-tauri/src/plugin.rs +++ b/src-tauri/src/plugin.rs @@ -8,6 +8,7 @@ use boa_engine::{ Context, JsArgs, JsNativeError, JsValue, Module, NativeFunction, Source, }; use boa_runtime::Console; +use log::info; use serde::{Deserialize, Serialize}; use serde_json::json; use sqlx::{Pool, Sqlite}; @@ -34,7 +35,7 @@ pub async fn run_plugin_import( file_path: &str, ) -> ImportedResources { let file = fs::read_to_string(file_path) - .expect(format!("Unable to read file {}", file_path.to_string()).as_str()); + .unwrap_or_else(|_| panic!("Unable to read file {}", file_path)); let file_contents = file.as_str(); let result_json = run_plugin( app_handle, @@ -46,41 +47,37 @@ pub async fn run_plugin_import( serde_json::from_value(result_json).expect("failed to parse result json"); let mut imported_resources = ImportedResources::default(); - println!("Importing resources"); + info!("Importing resources"); for w in resources.workspaces { - println!("Importing workspace: {:?}", w); - let x = models::upsert_workspace(&pool, w) + let x = models::upsert_workspace(pool, w) .await .expect("Failed to create workspace"); imported_resources.workspaces.push(x.clone()); - println!("Imported workspace: {}", x.name); + info!("Imported workspace: {}", x.name); } for e in resources.environments { - println!("Importing environment: {:?}", e); - let x = models::upsert_environment(&pool, e) + let x = models::upsert_environment(pool, e) .await .expect("Failed to create environment"); imported_resources.environments.push(x.clone()); - println!("Imported environment: {}", x.name); + info!("Imported environment: {}", x.name); } for f in resources.folders { - println!("Importing folder: {:?}", f); - let x = models::upsert_folder(&pool, f) + let x = models::upsert_folder(pool, f) .await .expect("Failed to create folder"); imported_resources.folders.push(x.clone()); - println!("Imported folder: {}", x.name); + info!("Imported folder: {}", x.name); } for r in resources.requests { - println!("Importing request: {:?}", r); - let x = models::upsert_request(&pool, r) + let x = models::upsert_request(pool, r) .await .expect("Failed to create request"); imported_resources.requests.push(x.clone()); - println!("Imported request: {}", x.name); + info!("Imported request: {}", x.name); } imported_resources diff --git a/src-web/components/Sidebar.tsx b/src-web/components/Sidebar.tsx index e1ca3a10..f8bb8412 100644 --- a/src-web/components/Sidebar.tsx +++ b/src-web/components/Sidebar.tsx @@ -4,6 +4,8 @@ import React, { forwardRef, Fragment, useCallback, useMemo, useRef, useState } f 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'; @@ -12,13 +14,15 @@ import { useCreateFolder } from '../hooks/useCreateFolder'; import { useCreateRequest } from '../hooks/useCreateRequest'; import { useDeleteAnyRequest } from '../hooks/useDeleteAnyRequest'; import { useDeleteFolder } from '../hooks/useDeleteFolder'; +import { useDeleteRequest } from '../hooks/useDeleteRequest'; import { useFolders } from '../hooks/useFolders'; 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 { useSendAnyRequest } from '../hooks/useSendAnyRequest'; +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'; @@ -489,10 +493,12 @@ const SidebarItem = forwardRef(function SidebarItem( }: SidebarItemProps, ref: ForwardedRef, ) { - const sendAnyRequest = useSendAnyRequest(); const createRequest = useCreateRequest(); const createFolder = useCreateFolder(); - const deleteRequest = useDeleteFolder(itemId); + const deleteFolder = useDeleteFolder(itemId); + const sendRequest = useSendRequest(itemId); + const sendManyRequests = useSendManyRequests(); + const deleteRequest = useDeleteRequest(itemId); const latestResponse = useLatestResponse(itemId); const updateRequest = useUpdateRequest(itemId); const updateAnyFolder = useUpdateAnyFolder(); @@ -543,9 +549,42 @@ const SidebarItem = forwardRef(function SidebarItem( [handleSubmitNameEdit], ); - const handleSelect = useCallback(() => { - onSelect(itemId); - }, [onSelect, itemId]); + const handleContextMenu = useCallback( + (e: React.MouseEvent) => { + 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], + ); + + const handleSelect = useCallback(() => onSelect(itemId), [onSelect, itemId]); return (
  • @@ -557,13 +596,7 @@ const SidebarItem = forwardRef(function SidebarItem( key: 'sendAll', label: 'Send All', leftSlot: , - onSelect: () => { - for (const { item } of child.children) { - if (item.model === 'http_request') { - sendAnyRequest.mutate(item.id); - } - } - }, + onSelect: () => sendManyRequests.mutate(child.children.map((c) => c.item.id)), }, { type: 'separator', label: itemName }, { @@ -590,7 +623,7 @@ const SidebarItem = forwardRef(function SidebarItem( label: 'Delete', variant: 'danger', leftSlot: , - onSelect: () => deleteRequest.mutate(), + onSelect: () => deleteFolder.mutate(), }, { type: 'separator' }, { @@ -617,9 +650,10 @@ const SidebarItem = forwardRef(function SidebarItem( )} - - - ); - }, - }); - - if (importedWorkspace != null) { - routes.navigate('workspace', { - workspaceId: importedWorkspace.id, - environmentId: imported.environments[0]?.id, - }); - } - }, [routes, dialog]); - const items: DropdownItem[] = useMemo(() => { const workspaceItems: DropdownItem[] = workspaces.map((w) => ({ key: w.id, @@ -193,30 +133,36 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({ { type: 'separator' }, { key: 'create-workspace', - label: 'Create Workspace', + label: 'New Workspace', leftSlot: , onSelect: async () => { const name = await prompt({ name: 'name', label: 'Name', defaultValue: 'My Workspace', - title: 'Create Workspace', + title: 'New Workspace', }); createWorkspace.mutate({ name }); }, }, - { - key: 'import', - label: 'Import Data', - onSelect: importData, - leftSlot: , - }, { key: 'appearance', label: 'Toggle Theme', onSelect: toggleAppearance, leftSlot: , }, + { + key: 'export-data', + label: 'Export Data', + leftSlot: , + onSelect: () => exportData.mutate(), + }, + { + key: 'import-data', + label: 'Import Data', + leftSlot: , + onSelect: () => importData.mutate(), + }, ]; }, [ activeWorkspace?.name, @@ -225,6 +171,7 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({ createWorkspace, deleteWorkspace.mutate, dialog, + exportData, importData, prompt, routes, diff --git a/src-web/components/core/Icon.tsx b/src-web/components/core/Icon.tsx index 6c62c0dc..3fac0563 100644 --- a/src-web/components/core/Icon.tsx +++ b/src-web/components/core/Icon.tsx @@ -37,6 +37,7 @@ import { TriangleLeftIcon, TriangleRightIcon, UpdateIcon, + UploadIcon, } from '@radix-ui/react-icons'; import classNames from 'classnames'; import type { HTMLAttributes } from 'react'; @@ -84,6 +85,7 @@ const icons = { triangleLeft: TriangleLeftIcon, triangleRight: TriangleRightIcon, update: UpdateIcon, + upload: UploadIcon, x: Cross2Icon, empty: (props: HTMLAttributes) => , }; diff --git a/src-web/hooks/useCreateEnvironment.ts b/src-web/hooks/useCreateEnvironment.ts index 568d0678..44f8eda4 100644 --- a/src-web/hooks/useCreateEnvironment.ts +++ b/src-web/hooks/useCreateEnvironment.ts @@ -20,7 +20,7 @@ export function useCreateEnvironment() { mutationFn: async () => { const name = await prompt({ name: 'name', - title: 'Create Environment', + title: 'New Environment', label: 'Name', defaultValue: 'My Environment', }); diff --git a/src-web/hooks/useExportData.tsx b/src-web/hooks/useExportData.tsx new file mode 100644 index 00000000..94be52b8 --- /dev/null +++ b/src-web/hooks/useExportData.tsx @@ -0,0 +1,31 @@ +import { useMutation } from '@tanstack/react-query'; +import { invoke } from '@tauri-apps/api'; +import type { OpenDialogOptions } from '@tauri-apps/api/dialog'; +import { open } from '@tauri-apps/api/dialog'; +import { useActiveWorkspaceId } from './useActiveWorkspaceId'; + +const openArgs: OpenDialogOptions = { + directory: true, + multiple: false, + title: 'Select Export Folder', +}; + +export function useExportData() { + const workspaceId = useActiveWorkspaceId(); + + return useMutation({ + mutationFn: async () => { + const selected = await open(openArgs); + if (selected == null) { + return; + } + + const rootDir = Array.isArray(selected) ? selected[0] : selected; + if (rootDir == null) { + return; + } + + await invoke('export_data', { workspaceId, rootDir }); + }, + }); +} diff --git a/src-web/hooks/useImportData.tsx b/src-web/hooks/useImportData.tsx new file mode 100644 index 00000000..7bd1d568 --- /dev/null +++ b/src-web/hooks/useImportData.tsx @@ -0,0 +1,70 @@ +import { useMutation } from '@tanstack/react-query'; +import { invoke } from '@tauri-apps/api'; +import type { OpenDialogOptions } from '@tauri-apps/api/dialog'; +import { open } from '@tauri-apps/api/dialog'; +import { Button } from '../components/core/Button'; +import { VStack } from '../components/core/Stacks'; +import { useDialog } from '../components/DialogContext'; +import type { Environment, Folder, HttpRequest, Workspace } from '../lib/models'; +import { count } from '../lib/pluralize'; +import { useAppRoutes } from './useAppRoutes'; + +const openArgs: OpenDialogOptions = { + filters: [{ name: 'Export File', extensions: ['json', 'yaml'] }], + multiple: false, +}; + +export function useImportData() { + const routes = useAppRoutes(); + const dialog = useDialog(); + + return useMutation({ + mutationFn: async () => { + const selected = await open(openArgs); + if (selected == null || selected.length === 0) { + return; + } + + const imported: { + workspaces: Workspace[]; + environments: Environment[]; + folders: Folder[]; + requests: HttpRequest[]; + } = await invoke('import_data', { + filePaths: Array.isArray(selected) ? selected : [selected], + }); + const importedWorkspace = imported.workspaces[0]; + + dialog.show({ + title: 'Import Complete', + size: 'sm', + hideX: true, + render: ({ hide }) => { + const { workspaces, environments, folders, requests } = imported; + return ( + +
      +
    • {count('Workspace', workspaces.length)}
    • +
    • {count('Environment', environments.length)}
    • +
    • {count('Folder', folders.length)}
    • +
    • {count('Request', requests.length)}
    • +
    +
    + +
    +
    + ); + }, + }); + + if (importedWorkspace != null) { + routes.navigate('workspace', { + workspaceId: importedWorkspace.id, + environmentId: imported.environments[0]?.id, + }); + } + }, + }); +} diff --git a/src-web/hooks/useSendFolder.ts b/src-web/hooks/useSendFolder.ts new file mode 100644 index 00000000..5a506ec3 --- /dev/null +++ b/src-web/hooks/useSendFolder.ts @@ -0,0 +1,15 @@ +import { useMutation } from '@tanstack/react-query'; +import { trackEvent } from '../lib/analytics'; +import { useSendAnyRequest } from './useSendAnyRequest'; + +export function useSendManyRequests() { + const sendAnyRequest = useSendAnyRequest(); + return useMutation({ + mutationFn: async (requestIds: string[]) => { + for (const id of requestIds) { + sendAnyRequest.mutate(id); + } + }, + onSettled: () => trackEvent('http_request', 'send'), + }); +} diff --git a/src-web/lib/pluralize.ts b/src-web/lib/pluralize.ts index 2bdf9784..a982c7b1 100644 --- a/src-web/lib/pluralize.ts +++ b/src-web/lib/pluralize.ts @@ -4,3 +4,7 @@ export function pluralize(word: string, count: number): string { } return `${word}s`; } + +export function count(word: string, count: number): string { + return `${count} ${pluralize(word, count)}`; +} diff --git a/src-web/main.tsx b/src-web/main.tsx index 2290a98d..088cb07f 100644 --- a/src-web/main.tsx +++ b/src-web/main.tsx @@ -1,13 +1,20 @@ import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; +import { attachConsole } from 'tauri-plugin-log-api'; import { App } from './components/App'; import { getKeyValue } from './lib/keyValueStore'; +import { maybeRestorePathname } from './lib/persistPathname'; import { getPreferredAppearance, setAppearance } from './lib/theme/window'; import './main.css'; -import { maybeRestorePathname } from './lib/persistPathname'; +await attachConsole(); await maybeRestorePathname(); +document.addEventListener('keydown', (e) => { + // Don't go back in history on backspace + if (e.key === 'Backspace') e.preventDefault(); +}); + setAppearance( await getKeyValue({ key: 'appearance',