Compare commits

...

14 Commits

Author SHA1 Message Date
Gregory Schier
4a58f73aa4 Oops 2024-04-02 10:11:37 +02:00
Gregory Schier
bd17650799 Postman text body import 2024-04-02 10:10:16 +02:00
Gregory Schier
db6a7dcabb Bump version 2024-04-01 08:48:26 +02:00
Gregory Schier
00b1f90074 Separate floating sidebar hidden state 2024-03-22 10:43:10 -07:00
Gregory Schier
e292235792 Filtering for cmd palette 2024-03-22 10:42:45 -07:00
Gregory Schier
5f86802d88 Space between var placeholders and code fold cursor 2024-03-22 10:42:35 -07:00
Gregory Schier
acb7f2e49b Fix Postman variable import 2024-03-22 10:40:51 -07:00
Gregory Schier
e2a15609bf Adjust highlight color 2024-03-22 10:37:45 -07:00
Gregory Schier
aa3bfd78c4 Some scrolling tweaks 2024-03-20 17:27:47 -07:00
Gregory Schier
23c4971127 Fix URL bar buttons in expanded state 2024-03-20 16:17:05 -07:00
Gregory Schier
40669217fb Bump version 2024-03-20 16:05:14 -07:00
Gregory Schier
8089ea87e8 Fix dialog height 2024-03-20 16:05:01 -07:00
Gregory Schier
9de24e3a40 Remove openOnHotKeyAction in favor of putting hotkey on the trigger button= 2024-03-20 15:56:39 -07:00
Gregory Schier
5afb8e7383 Use SQLite connect options 2024-03-20 13:33:11 -07:00
25 changed files with 244 additions and 211 deletions

View File

@@ -37,10 +37,11 @@ export function pluginHookImport(contents: string): { resources: ExportResources
id: generateId('wk'),
name: info.name || 'Postman Import',
description: info.description || '',
variables: root.variable?.map((v: any) => ({
name: v.key,
value: v.value,
})),
variables:
root.variable?.map((v: any) => ({
name: v.key,
value: v.value,
})) ?? [],
};
exportResources.workspaces.push(workspace);
@@ -190,6 +191,20 @@ function importBody(rawBody: any): Pick<HttpRequest, 'body' | 'bodyType' | 'head
),
},
};
} else if ('raw' in body) {
return {
headers: [
{
name: 'Content-Type',
value: body.options?.raw?.language === 'json' ? 'application/json' : '',
enabled: true,
},
],
bodyType: body.options?.raw?.language === 'json' ? 'application/json' : 'other',
body: {
text: body.raw ?? '',
},
};
} else {
// TODO: support other body types
return { headers: [], bodyType: null, body: {} };

80
src-tauri/Cargo.lock generated
View File

@@ -19,9 +19,9 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "ahash"
version = "0.8.6"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a"
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
dependencies = [
"cfg-if",
"getrandom 0.2.11",
@@ -161,16 +161,6 @@ dependencies = [
"num-traits",
]
[[package]]
name = "atomic-write-file"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edcdbedc2236483ab103a53415653d6b4442ea6141baf1ffa85df29635e88436"
dependencies = [
"nix",
"rand 0.8.5",
]
[[package]]
name = "autocfg"
version = "1.1.0"
@@ -249,6 +239,12 @@ version = "0.21.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9"
[[package]]
name = "base64"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51"
[[package]]
name = "base64ct"
version = "1.6.0"
@@ -2654,17 +2650,6 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54"
[[package]]
name = "nix"
version = "0.27.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053"
dependencies = [
"bitflags 2.4.1",
"cfg-if",
"libc",
]
[[package]]
name = "nodrop"
version = "0.1.14"
@@ -4314,9 +4299,9 @@ dependencies = [
[[package]]
name = "sqlx"
version = "0.7.3"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dba03c279da73694ef99763320dea58b51095dfe87d001b1d4b5fe78ba8763cf"
checksum = "c9a2ccff1a000a5a59cd33da541d9f2fdcd9e6e8229cc200565942bff36d0aaa"
dependencies = [
"sqlx-core",
"sqlx-macros",
@@ -4327,9 +4312,9 @@ dependencies = [
[[package]]
name = "sqlx-core"
version = "0.7.3"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d84b0a3c3739e220d94b3239fd69fb1f74bc36e16643423bd99de3b43c21bfbd"
checksum = "24ba59a9342a3d9bab6c56c118be528b27c9b60e490080e9711a04dccac83ef6"
dependencies = [
"ahash",
"atoi",
@@ -4338,7 +4323,6 @@ dependencies = [
"chrono",
"crc",
"crossbeam-queue",
"dotenvy",
"either",
"event-listener",
"futures-channel",
@@ -4372,9 +4356,9 @@ dependencies = [
[[package]]
name = "sqlx-macros"
version = "0.7.3"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89961c00dc4d7dffb7aee214964b065072bff69e36ddb9e2c107541f75e4f2a5"
checksum = "4ea40e2345eb2faa9e1e5e326db8c34711317d2b5e08d0d5741619048a803127"
dependencies = [
"proc-macro2",
"quote",
@@ -4385,11 +4369,10 @@ dependencies = [
[[package]]
name = "sqlx-macros-core"
version = "0.7.3"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0bd4519486723648186a08785143599760f7cc81c52334a55d6a83ea1e20841"
checksum = "5833ef53aaa16d860e92123292f1f6a3d53c34ba8b1969f152ef1a7bb803f3c8"
dependencies = [
"atomic-write-file",
"dotenvy",
"either",
"heck 0.4.1",
@@ -4412,9 +4395,9 @@ dependencies = [
[[package]]
name = "sqlx-mysql"
version = "0.7.3"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e37195395df71fd068f6e2082247891bc11e3289624bbc776a0cdfa1ca7f1ea4"
checksum = "1ed31390216d20e538e447a7a9b959e06ed9fc51c37b514b46eb758016ecd418"
dependencies = [
"atoi",
"base64 0.21.5",
@@ -4456,9 +4439,9 @@ dependencies = [
[[package]]
name = "sqlx-postgres"
version = "0.7.3"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6ac0ac3b7ccd10cc96c7ab29791a7dd236bd94021f31eec7ba3d46a74aa1c24"
checksum = "7c824eb80b894f926f89a0b9da0c7f435d27cdd35b8c655b114e58223918577e"
dependencies = [
"atoi",
"base64 0.21.5",
@@ -4484,7 +4467,6 @@ dependencies = [
"rand 0.8.5",
"serde",
"serde_json",
"sha1",
"sha2",
"smallvec",
"sqlx-core",
@@ -4497,9 +4479,9 @@ dependencies = [
[[package]]
name = "sqlx-sqlite"
version = "0.7.3"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "210976b7d948c7ba9fced8ca835b11cbb2d677c59c79de41ac0d397e14547490"
checksum = "b244ef0a8414da0bed4bb1910426e890b19e5e9bccc27ada6b797d05c55ae0aa"
dependencies = [
"atoi",
"chrono",
@@ -5123,9 +5105,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.34.0"
version = "1.36.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0c014766411e834f7af5b8f4cf46257aab4036ca95e9d2c144a10f59ad6f5b9"
checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931"
dependencies = [
"backtrace",
"bytes",
@@ -5181,9 +5163,9 @@ dependencies = [
[[package]]
name = "tokio-stream"
version = "0.1.14"
version = "0.1.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842"
checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af"
dependencies = [
"futures-core",
"pin-project-lite",
@@ -6332,7 +6314,7 @@ dependencies = [
name = "yaak-app"
version = "0.0.0"
dependencies = [
"base64 0.21.5",
"base64 0.22.0",
"boa_engine",
"boa_runtime",
"chrono",
@@ -6387,18 +6369,18 @@ dependencies = [
[[package]]
name = "zerocopy"
version = "0.7.25"
version = "0.7.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8cd369a67c0edfef15010f980c3cbe45d7f651deac2cd67ce097cd801de16557"
checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.7.25"
version = "0.7.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2f140bda219a26ccc0cdb03dba58af72590c53b22642577d88a927bc5c87d6b"
checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
dependencies = [
"proc-macro2",
"quote",

View File

@@ -22,7 +22,7 @@ cocoa = "0.25.0"
openssl-sys = { version = "0.9", features = ["vendored"] } # For Ubuntu installation to work
[dependencies]
base64 = "0.21.0"
base64 = "0.22.0"
boa_engine = { version = "0.17.3", features = ["annex-b"] }
boa_runtime = { version = "0.17.3" }
chrono = { version = "0.4.31", features = ["serde"] }
@@ -33,7 +33,7 @@ reqwest = { version = "0.11.23", features = ["multipart", "cookies", "gzip", "br
cookie = { version = "0.18.0" }
serde = { version = "1.0.195", features = ["derive"] }
serde_json = { version = "1.0.111", features = ["raw_value"] }
sqlx = { version = "0.7.3", features = ["sqlite", "runtime-tokio-rustls", "json", "chrono", "time"] }
sqlx = { version = "0.7.4", features = ["sqlite", "runtime-tokio-rustls", "json", "chrono", "time"] }
tauri = { version = "1.5.4", features = [
"config-toml",
"path-all",
@@ -56,14 +56,14 @@ tauri = { version = "1.5.4", features = [
] }
tauri-plugin-window-state = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
tauri-plugin-log = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1", features = ["colored"] }
tokio = { version = "1.25.0", features = ["sync"] }
tokio = { version = "1.36.0", features = ["sync"] }
uuid = "1.3.0"
log = "0.4.20"
datetime = "0.5.2"
window-shadows = "0.2.2"
reqwest_cookie_store = "0.6.0"
grpc = { path = "./grpc" }
tokio-stream = "0.1.14"
tokio-stream = "0.1.15"
[features]
# by default Tauri runs in production mode

View File

@@ -1,73 +1,73 @@
const q = "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", S = "https://schema.getpostman.com/json/collection/v2.0.0/collection.json", _ = [S, q];
function v(t) {
var b;
const e = w(t);
function j(t) {
var g;
const e = A(t);
if (e == null)
return;
const n = o(e.info);
if (!_.includes(n.schema) || !Array.isArray(e.item))
const a = l(e.info);
if (!_.includes(a.schema) || !Array.isArray(e.item))
return;
const A = g(e.auth), i = {
const c = w(e.auth), s = {
workspaces: [],
environments: [],
httpRequests: [],
folders: []
}, c = {
}, n = {
model: "workspace",
id: m("wk"),
name: n.name || "Postman Import",
description: n.description || "",
variables: (b = e.variable) == null ? void 0 : b.map((r) => ({
id: y("wk"),
name: a.name || "Postman Import",
description: a.description || "",
variables: ((g = e.variable) == null ? void 0 : g.map((r) => ({
name: r.key,
value: r.value
}))
}))) ?? []
};
i.workspaces.push(c);
const f = (r, u = null) => {
s.workspaces.push(n);
const f = (r, d = null) => {
if (typeof r.name == "string" && Array.isArray(r.item)) {
const a = {
const o = {
model: "folder",
workspaceId: c.id,
id: m("fl"),
workspaceId: n.id,
id: y("fl"),
name: r.name,
folderId: u
folderId: d
};
i.folders.push(a);
for (const s of r.item)
f(s, a.id);
s.folders.push(o);
for (const u of r.item)
f(u, o.id);
} else if (typeof r.name == "string" && "request" in r) {
const a = o(r.request), s = O(a.body), T = g(a.auth), d = T.authenticationType == null ? A : T, k = {
const o = l(r.request), u = O(o.body), T = w(o.auth), p = T.authenticationType == null ? c : T, k = {
model: "http_request",
id: m("rq"),
workspaceId: c.id,
folderId: u,
id: y("rq"),
workspaceId: n.id,
folderId: d,
name: r.name,
method: a.method || "GET",
url: typeof a.url == "string" ? a.url : o(a.url).raw,
body: s.body,
bodyType: s.bodyType,
authentication: d.authentication,
authenticationType: d.authenticationType,
method: o.method || "GET",
url: typeof o.url == "string" ? o.url : l(o.url).raw,
body: u.body,
bodyType: u.bodyType,
authentication: p.authentication,
authenticationType: p.authenticationType,
headers: [
...s.headers,
...d.headers,
...y(a.header).map((p) => ({
name: p.key,
value: p.value,
enabled: !p.disabled
...u.headers,
...p.headers,
...h(o.header).map((m) => ({
name: m.key,
value: m.value,
enabled: !m.disabled
}))
]
};
i.httpRequests.push(k);
s.httpRequests.push(k);
} else
console.log("Unknown item", r, u);
console.log("Unknown item", r, d);
};
for (const r of e.item)
f(r);
return { resources: h(i) };
return { resources: b(s) };
}
function g(t) {
const e = o(t);
function w(t) {
const e = l(t);
return "basic" in e ? {
headers: [],
authenticationType: "basic",
@@ -84,7 +84,8 @@ function g(t) {
} : { headers: [], authenticationType: null, authentication: {} };
}
function O(t) {
const e = o(t);
var a, i, c, s;
const e = l(t);
return "graphql" in e ? {
headers: [
{
@@ -96,7 +97,7 @@ function O(t) {
bodyType: "graphql",
body: {
text: JSON.stringify(
{ query: e.graphql.query, variables: w(e.graphql.variables) },
{ query: e.graphql.query, variables: A(e.graphql.variables) },
null,
2
)
@@ -111,7 +112,7 @@ function O(t) {
],
bodyType: "application/x-www-form-urlencoded",
body: {
form: y(e.urlencoded).map((n) => ({
form: h(e.urlencoded).map((n) => ({
enabled: !n.disabled,
name: n.key ?? "",
value: n.value ?? ""
@@ -127,7 +128,7 @@ function O(t) {
],
bodyType: "multipart/form-data",
body: {
form: y(e.formdata).map(
form: h(e.formdata).map(
(n) => n.src != null ? {
enabled: !n.disabled,
name: n.key ?? "",
@@ -139,34 +140,46 @@ function O(t) {
}
)
}
} : "raw" in e ? {
headers: [
{
name: "Content-Type",
value: ((i = (a = e.options) == null ? void 0 : a.raw) == null ? void 0 : i.language) === "json" ? "application/json" : "",
enabled: !0
}
],
bodyType: ((s = (c = e.options) == null ? void 0 : c.raw) == null ? void 0 : s.language) === "json" ? "application/json" : "other",
body: {
text: e.raw ?? ""
}
} : { headers: [], bodyType: null, body: {} };
}
function w(t) {
function A(t) {
try {
return o(JSON.parse(t));
return l(JSON.parse(t));
} catch {
}
return null;
}
function o(t) {
function l(t) {
return Object.prototype.toString.call(t) === "[object Object]" ? t : {};
}
function y(t) {
function h(t) {
return Object.prototype.toString.call(t) === "[object Array]" ? t : [];
}
function h(t) {
return typeof t == "string" ? t.replace(/{{\s*(_\.)?([^}]+)\s*}}/g, "${[$2]}") : Array.isArray(t) && t != null ? t.map(h) : typeof t == "object" && t != null ? Object.fromEntries(
Object.entries(t).map(([e, n]) => [e, h(n)])
function b(t) {
return typeof t == "string" ? t.replace(/{{\s*(_\.)?([^}]+)\s*}}/g, "${[$2]}") : Array.isArray(t) && t != null ? t.map(b) : typeof t == "object" && t != null ? Object.fromEntries(
Object.entries(t).map(([e, a]) => [e, b(a)])
) : t;
}
function m(t) {
function y(t) {
const e = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
let n = `${t}_`;
for (let l = 0; l < 10; l++)
n += e[Math.floor(Math.random() * e.length)];
return n;
let a = `${t}_`;
for (let i = 0; i < 10; i++)
a += e[Math.floor(Math.random() * e.length)];
return a;
}
export {
m as generateId,
v as pluginHookImport
y as generateId,
j as pluginHookImport
};

View File

@@ -23,6 +23,7 @@ use log::{debug, error, info, warn};
use rand::random;
use serde_json::{json, Value};
use sqlx::migrate::Migrator;
use sqlx::sqlite::{SqliteConnectOptions};
use sqlx::types::Json;
use sqlx::{Pool, Sqlite, SqlitePool};
#[cfg(target_os = "macos")]
@@ -749,7 +750,7 @@ async fn cmd_import_data(
let mut imported_resources = WorkspaceExportResources::default();
info!("Importing resources");
for mut v in r.resources.workspaces {
for v in r.resources.workspaces {
let x = upsert_workspace(&w, v).await.map_err(|e| e.to_string())?;
imported_resources.workspaces.push(x.clone());
info!("Imported workspace: {}", x.name);
@@ -1430,7 +1431,8 @@ fn main() {
// Add DB handle
tauri::async_runtime::block_on(async move {
let pool = SqlitePool::connect(p.to_str().unwrap())
let opts = SqliteConnectOptions::from_str(p.to_str().unwrap()).unwrap();
let pool = SqlitePool::connect_with(opts)
.await
.expect("Failed to connect to database");
let m = Mutex::new(pool.clone());

View File

@@ -8,7 +8,7 @@
},
"package": {
"productName": "Yaak",
"version": "2024.3.7"
"version": "2024.3.10"
},
"tauri": {
"windows": [],

View File

@@ -13,7 +13,7 @@ export function BasicAuth<T extends HttpRequest | GrpcRequest>({ request }: Prop
const updateGrpcRequest = useUpdateGrpcRequest(request.id);
return (
<VStack className="my-2" space={2}>
<VStack className="py-2 overflow-y-auto h-full" space={2}>
<Input
useTemplating
autocompleteVariables

View File

@@ -15,6 +15,7 @@ export function CommandPalette({ onClose }: { onClose: () => void }) {
const activeEnvironmentId = useActiveEnvironmentId();
const workspaces = useWorkspaces();
const requests = useRequests();
const [command, setCommand] = useState<string>('');
const items = useMemo<{ label: string; onSelect: () => void; key: string }[]>(() => {
const items = [];
@@ -47,10 +48,17 @@ export function CommandPalette({ onClose }: { onClose: () => void }) {
return items;
}, [activeEnvironmentId, requests, routes, workspaces]);
const handleSelectAndClose = (cb: () => void) => {
onClose();
cb();
};
const filteredItems = useMemo(() => {
return items.filter((v) => v.label.toLowerCase().includes(command.toLowerCase()));
}, [command, items]);
const handleSelectAndClose = useCallback(
(cb: () => void) => {
onClose();
cb();
},
[onClose],
);
const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
@@ -59,13 +67,13 @@ export function CommandPalette({ onClose }: { onClose: () => void }) {
} else if (e.key === 'ArrowUp') {
setSelectedIndex((prev) => prev - 1);
} else if (e.key === 'Enter') {
const item = items[selectedIndex];
const item = filteredItems[selectedIndex];
if (item) {
handleSelectAndClose(item.onSelect);
}
}
},
[items, onClose, selectedIndex],
[filteredItems, handleSelectAndClose, selectedIndex],
);
return (
@@ -76,11 +84,13 @@ export function CommandPalette({ onClose }: { onClose: () => void }) {
name="command"
label="Command"
placeholder="Type a command"
defaultValue=""
onChange={setCommand}
onKeyDown={handleKeyDown}
/>
</div>
<div className="h-full px-1.5 overflow-y-auto">
{items.map((v, i) => (
{filteredItems.map((v, i) => (
<CommandPaletteItem
active={i === selectedIndex}
key={v.key}

View File

@@ -5,14 +5,9 @@ import { Dropdown } from './core/Dropdown';
interface Props {
hideFolder?: boolean;
children: DropdownProps['children'];
openOnHotKeyAction?: DropdownProps['openOnHotKeyAction'];
}
export function CreateDropdown({ hideFolder, children, openOnHotKeyAction }: Props) {
export function CreateDropdown({ hideFolder, children }: Props) {
const items = useCreateDropdownItems({ hideFolder, hideIcons: true });
return (
<Dropdown openOnHotKeyAction={openOnHotKeyAction} items={items}>
{children}
</Dropdown>
);
return <Dropdown items={items}>{children}</Dropdown>;
}

View File

@@ -41,10 +41,6 @@ export function RecentRequestsDropdown({ className }: Pick<ButtonProps, 'classNa
dropdownRef.current?.prev?.();
});
useHotKey('request_switcher.toggle', () => {
dropdownRef.current?.toggle();
});
const items = useMemo<DropdownItem[]>(() => {
if (activeWorkspaceId === null) return [];
@@ -87,6 +83,7 @@ export function RecentRequestsDropdown({ className }: Pick<ButtonProps, 'classNa
<Button
data-tauri-drag-region
size="sm"
hotkeyAction="request_switcher.toggle"
className={classNames(
className,
'text-gray-800 text-sm truncate pointer-events-auto',

View File

@@ -346,7 +346,7 @@ export const RequestPane = memo(function RequestPane({
onChangeContentType={handleContentTypeChange}
/>
) : (
<EmptyStateText>No Body</EmptyStateText>
<EmptyStateText>Empty Body</EmptyStateText>
)}
</TabContent>
</Tabs>

View File

@@ -56,7 +56,7 @@ interface TreeNode {
}
export function Sidebar({ className }: Props) {
const { hidden, show, hide } = useSidebarHidden();
const [hidden, setHidden] = useSidebarHidden();
const sidebarRef = useRef<HTMLLIElement>(null);
const activeRequest = useActiveRequest();
const activeEnvironmentId = useActiveEnvironmentId();
@@ -241,16 +241,15 @@ export function Sidebar({ className }: Props) {
useKeyPressEvent('Delete', handleDeleteKey);
useHotKey('sidebar.focus', async () => {
console.log('sidebar.focus', { hidden, hasFocus });
// Hide the sidebar if it's already focused
if (!hidden && hasFocus) {
await hide();
await setHidden(true);
return;
}
// Show the sidebar if it's hidden
if (hidden) {
await show();
await setHidden(false);
}
// Select 0 index on focus if none selected

View File

@@ -1,12 +1,22 @@
import { memo } from 'react';
import { useMemo } from 'react';
import { useFloatingSidebarHidden } from '../hooks/useFloatingSidebarHidden';
import { useShouldFloatSidebar } from '../hooks/useShouldFloatSidebar';
import { useSidebarHidden } from '../hooks/useSidebarHidden';
import { trackEvent } from '../lib/analytics';
import { IconButton } from './core/IconButton';
import { HStack } from './core/Stacks';
import { CreateDropdown } from './CreateDropdown';
export const SidebarActions = memo(function SidebarActions() {
const { hidden, show, hide } = useSidebarHidden();
export function SidebarActions() {
const floating = useShouldFloatSidebar();
const [normalHidden, setNormalHidden] = useSidebarHidden();
const [floatingHidden, setFloatingHidden] = useFloatingSidebarHidden();
const hidden = floating ? floatingHidden : normalHidden;
const setHidden = useMemo(
() => (floating ? setFloatingHidden : setNormalHidden),
[floating, setFloatingHidden, setNormalHidden],
);
return (
<HStack className="h-full" alignItems="center">
@@ -14,10 +24,9 @@ export const SidebarActions = memo(function SidebarActions() {
onClick={async () => {
trackEvent('sidebar', 'toggle');
// NOTE: We're not using `toggle` because it may be out of sync
// from changes in other windows
if (hidden) await show();
else await hide();
// NOTE: We're not using the (h) => !h pattern here because the data
// might be different if another window changed it (out of sync)
await setHidden(!hidden);
}}
className="pointer-events-auto"
size="sm"
@@ -25,9 +34,14 @@ export const SidebarActions = memo(function SidebarActions() {
hotkeyAction="sidebar.toggle"
icon={hidden ? 'leftPanelHidden' : 'leftPanelVisible'}
/>
<CreateDropdown openOnHotKeyAction="http_request.create">
<IconButton size="sm" icon="plusCircle" title="Add Resource" />
<CreateDropdown>
<IconButton
size="sm"
icon="plusCircle"
title="Add Resource"
hotkeyAction="http_request.create"
/>
</CreateDropdown>
</HStack>
);
});
}

View File

@@ -60,7 +60,7 @@ export const UrlBar = memo(function UrlBar({
hideLabel
useTemplating
contentType="url"
className="px-0 py-0.5"
className="pl-0 pr-1.5 py-0.5"
name="url"
label="Enter URL"
forceUpdateKey={forceUpdateKey}
@@ -76,7 +76,7 @@ export const UrlBar = memo(function UrlBar({
<RequestMethodDropdown
method={method}
onChange={onMethodChange}
className="!h-auto my-0.5 ml-0.5"
className="my-0.5 ml-0.5"
/>
)
}
@@ -87,7 +87,7 @@ export const UrlBar = memo(function UrlBar({
iconSize="md"
title="Send Request"
type="submit"
className="w-8 !h-auto my-0.5 mr-0.5"
className="w-8 my-0.5 mr-0.5"
icon={isLoading ? 'x' : submitIcon}
hotkeyAction="http_request.send"
/>

View File

@@ -6,14 +6,16 @@ import type {
MouseEvent as ReactMouseEvent,
ReactNode,
} from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useCallback, useMemo, useRef, useState } from 'react';
import { useWindowSize } from 'react-use';
import { useActiveRequest } from '../hooks/useActiveRequest';
import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
import { useActiveWorkspaceId } from '../hooks/useActiveWorkspaceId';
import { useFloatingSidebarHidden } from '../hooks/useFloatingSidebarHidden';
import { useImportData } from '../hooks/useImportData';
import { useIsFullscreen } from '../hooks/useIsFullscreen';
import { useOsInfo } from '../hooks/useOsInfo';
import { useShouldFloatSidebar } from '../hooks/useShouldFloatSidebar';
import { useSidebarHidden } from '../hooks/useSidebarHidden';
import { useSidebarWidth } from '../hooks/useSidebarWidth';
import { useWorkspaces } from '../hooks/useWorkspaces';
@@ -37,34 +39,22 @@ const head = { gridArea: 'head' };
const body = { gridArea: 'body' };
const drag = { gridArea: 'drag' };
const WINDOW_FLOATING_SIDEBAR_WIDTH = 600;
export default function Workspace() {
const workspaces = useWorkspaces();
const activeWorkspace = useActiveWorkspace();
const activeWorkspaceId = useActiveWorkspaceId();
const { setWidth, width, resetWidth } = useSidebarWidth();
const { hide, show, hidden } = useSidebarHidden();
const [sidebarHidden, setSidebarHidden] = useSidebarHidden();
const [floatingSidebarHidden, setFloatingSidebarHidden] = useFloatingSidebarHidden();
const activeRequest = useActiveRequest();
const windowSize = useWindowSize();
const importData = useImportData();
const [floating, setFloating] = useState<boolean>(false);
const floating = useShouldFloatSidebar();
const [isResizing, setIsResizing] = useState<boolean>(false);
const moveState = useRef<{ move: (e: MouseEvent) => void; up: (e: MouseEvent) => void } | null>(
null,
);
// float/un-float sidebar on window resize
useEffect(() => {
const shouldHide = windowSize.width <= WINDOW_FLOATING_SIDEBAR_WIDTH;
if (shouldHide && !floating) {
setFloating(true);
hide().catch(console.error);
} else if (!shouldHide && floating) {
setFloating(false);
}
}, [floating, hide, windowSize.width]);
const unsub = () => {
if (moveState.current !== null) {
document.documentElement.removeEventListener('mousemove', moveState.current.move);
@@ -84,10 +74,10 @@ export default function Workspace() {
e.preventDefault(); // Prevent text selection and things
const newWidth = startWidth + (e.clientX - mouseStartX);
if (newWidth < 50) {
await hide();
await setSidebarHidden(true);
resetWidth();
} else {
await show();
await setSidebarHidden(false);
setWidth(newWidth);
}
},
@@ -101,10 +91,10 @@ export default function Workspace() {
document.documentElement.addEventListener('mouseup', moveState.current.up);
setIsResizing(true);
},
[setWidth, resetWidth, width, hide, show],
[width, setSidebarHidden, resetWidth, setWidth],
);
const sideWidth = hidden ? 0 : width;
const sideWidth = sidebarHidden ? 0 : width;
const styles = useMemo<CSSProperties>(
() => ({
gridTemplate: floating
@@ -144,7 +134,11 @@ export default function Workspace() {
)}
>
{floating ? (
<Overlay open={!hidden} portalName="sidebar" onClose={hide}>
<Overlay
open={!floatingSidebarHidden}
portalName="sidebar"
onClose={() => setFloatingSidebarHidden(true)}
>
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}

View File

@@ -16,7 +16,7 @@ export function Banner({ children, className, color = 'gray' }: Props) {
color === 'gray' && 'border-gray-500/60 bg-gray-300/10 text-gray-800',
color === 'warning' && 'border-orange-500/60 bg-orange-300/10 text-orange-800',
color === 'danger' && 'border-red-500/60 bg-red-300/10 text-red-800',
color === 'success' && 'border-green-500/60 bg-green-300/10 text-green-800',
color === 'success' && 'border-violet-500/60 bg-violet-300/10 text-violet-800',
)}
>
{children}

View File

@@ -34,8 +34,8 @@ export function Checkbox({
<input
aria-hidden
className={classNames(
'opacity-50 appearance-none w-4 h-4 flex-shrink-0 border border-[currentColor]',
'rounded hocus:border-focus hocus:bg-focus/[5%] hocus:opacity-100 outline-none ring-0',
'appearance-none w-4 h-4 flex-shrink-0 border border-highlight',
'rounded hocus:border-focus hocus:bg-focus/[5%] outline-none ring-0',
)}
type="checkbox"
disabled={disabled}

View File

@@ -62,7 +62,7 @@ export function Dialog({
animate={{ top: 0, scale: 1 }}
className={classNames(
className,
'h-full grid grid-rows-[auto_auto_minmax(0,1fr)]',
'grid grid-rows-[auto_auto_minmax(0,1fr)]',
'relative bg-gray-50 pointer-events-auto',
'rounded-lg',
'dark:border border-highlight shadow shadow-black/10',

View File

@@ -55,7 +55,6 @@ export type DropdownItem = DropdownItemDefault | DropdownItemSeparator;
export interface DropdownProps {
children: ReactElement<HTMLAttributes<HTMLButtonElement>>;
items: DropdownItem[];
openOnHotKeyAction?: HotkeyAction;
onOpen?: () => void;
onClose?: () => void;
}
@@ -71,7 +70,7 @@ export interface DropdownRef {
}
export const Dropdown = forwardRef<DropdownRef, DropdownProps>(function Dropdown(
{ children, items, openOnHotKeyAction, onOpen, onClose }: DropdownProps,
{ children, items, onOpen, onClose }: DropdownProps,
ref,
) {
const [isOpen, _setIsOpen] = useState<boolean>(false);
@@ -88,11 +87,6 @@ export const Dropdown = forwardRef<DropdownRef, DropdownProps>(function Dropdown
[onClose, onOpen],
);
useHotKey(openOnHotKeyAction ?? null, () => {
setIsOpen(true);
menuRef.current?.next?.();
});
useImperativeHandle(ref, () => ({
...menuRef.current,
isOpen: isOpen,

View File

@@ -55,7 +55,7 @@
}
.placeholder-widget {
@apply text-xs text-violet-700 dark:text-violet-700 px-1 rounded cursor-default dark:shadow;
@apply text-xs text-violet-700 dark:text-violet-700 px-1 mx-[0.5px] rounded cursor-default dark:shadow;
/* NOTE: Background and border are translucent so we can see text selection through it */
@apply bg-violet-500/20 border border-violet-500/20 border-opacity-40;
@@ -131,7 +131,8 @@
}
.cm-editor .fold-gutter-icon {
@apply pt-[0.25em] pl-[0.4em] px-[0.4em] h-4 cursor-pointer rounded;
@apply pt-[0.25em] pl-[0.4em] px-[0.4em] h-4 rounded;
@apply cursor-default !important;
}
.cm-editor .fold-gutter-icon::after {
@@ -152,8 +153,9 @@
}
.cm-editor .cm-foldPlaceholder {
@apply px-2 border border-gray-400/50 bg-gray-300/50 cursor-default;
@apply px-2 border border-gray-400/50 bg-gray-300/50;
@apply hover:text-gray-800 hover:border-gray-400;
@apply cursor-default !important;
}
.cm-editor .cm-activeLineGutter {

View File

@@ -32,6 +32,7 @@ export type PairEditorProps = {
allowFileValues?: boolean;
nameValidate?: InputProps['validate'];
valueValidate?: InputProps['validate'];
noScroll?: boolean;
};
export type Pair = {
@@ -57,6 +58,7 @@ export const PairEditor = memo(function PairEditor({
nameValidate,
valueType,
onChange,
noScroll,
pairs: originalPairs,
valueAutocomplete,
valueAutocompleteVariables,
@@ -95,7 +97,7 @@ export const PairEditor = memo(function PairEditor({
[onChange],
);
const handleMove = useCallback<FormRowProps['onMove']>(
const handleMove = useCallback<PairEditorRowProps['onMove']>(
(id, side) => {
const dragIndex = pairs.findIndex((r) => r.id === id);
setHoveredIndex(side === 'above' ? dragIndex : dragIndex + 1);
@@ -103,7 +105,7 @@ export const PairEditor = memo(function PairEditor({
[pairs],
);
const handleEnd = useCallback<FormRowProps['onEnd']>(
const handleEnd = useCallback<PairEditorRowProps['onEnd']>(
(id: string) => {
if (hoveredIndex === null) return;
setHoveredIndex(null);
@@ -162,7 +164,8 @@ export const PairEditor = memo(function PairEditor({
className={classNames(
className,
'@container',
'pb-2 grid overflow-auto max-h-full',
'pb-2 mb-auto',
!noScroll && 'overflow-y-auto max-h-full',
// Move over the width of the drag handle
'-ml-3',
// Pad to make room for the drag divider
@@ -174,7 +177,7 @@ export const PairEditor = memo(function PairEditor({
return (
<Fragment key={p.id}>
{hoveredIndex === i && <DropMarker />}
<FormRow
<PairEditorRow
pairContainer={p}
className="py-1"
isLast={isLast}
@@ -207,7 +210,7 @@ enum ItemTypes {
ROW = 'pair-row',
}
type FormRowProps = {
type PairEditorRowProps = {
className?: string;
pairContainer: PairContainer;
forceFocusPairId?: string | null;
@@ -233,7 +236,7 @@ type FormRowProps = {
| 'allowFileValues'
>;
const FormRow = memo(function FormRow({
function PairEditorRow({
allowFileValues,
className,
forceFocusPairId,
@@ -254,7 +257,7 @@ const FormRow = memo(function FormRow({
valuePlaceholder,
valueValidate,
valueType,
}: FormRowProps) {
}: PairEditorRowProps) {
const { id } = pairContainer;
const ref = useRef<HTMLDivElement>(null);
const prompt = usePrompt();
@@ -480,7 +483,7 @@ const FormRow = memo(function FormRow({
)}
</div>
);
});
}
const newPairContainer = (initialPair?: Pair): PairContainer => {
const id = initialPair?.id ?? uuid();

View File

@@ -0,0 +1,13 @@
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
import { useKeyValue } from './useKeyValue';
export function useFloatingSidebarHidden() {
const activeWorkspaceId = useActiveWorkspaceId();
const { set, value } = useKeyValue<boolean>({
namespace: 'no_sync',
key: ['floating_sidebar_hidden', activeWorkspaceId ?? 'n/a'],
fallback: false,
});
return [value, set] as const;
}

View File

@@ -0,0 +1,8 @@
import { useWindowSize } from 'react-use';
const WINDOW_FLOATING_SIDEBAR_WIDTH = 600;
export function useShouldFloatSidebar() {
const windowSize = useWindowSize();
return windowSize.width <= WINDOW_FLOATING_SIDEBAR_WIDTH;
}

View File

@@ -1,4 +1,3 @@
import { useMemo } from 'react';
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
import { useKeyValue } from './useKeyValue';
@@ -10,12 +9,5 @@ export function useSidebarHidden() {
fallback: false,
});
return useMemo(() => {
return {
show: () => set(false),
hide: () => set(true),
toggle: () => set((h) => !h),
hidden: value,
};
}, [set, value]);
return [value, set] as const;
}

View File

@@ -63,8 +63,8 @@ module.exports = {
selection: 'hsl(var(--color-violet-500) / 0.3)',
focus: 'hsl(var(--color-blue-500) / 0.7)',
invalid: 'hsl(var(--color-red-500))',
highlight: 'hsl(var(--color-gray-300) / 0.35)',
highlightSecondary: 'hsl(var(--color-gray-300) / 0.2)',
highlight: 'hsl(var(--color-gray-500) / 0.3)',
highlightSecondary: 'hsl(var(--color-gray-500) / 0.15)',
transparent: 'transparent',
white: 'hsl(0 100% 100% / <alpha-value>)',
black: 'hsl(0 100% 0% / <alpha-value>)',