mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-01-18 06:57:11 +01:00
Compare commits
20 Commits
v2024.3.0-
...
v2024.3.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cefdc3ecf3 | ||
|
|
02960d2d64 | ||
|
|
9e5226aa83 | ||
|
|
63d7a44586 | ||
|
|
c851dfe206 | ||
|
|
6adc15a249 | ||
|
|
9ac7aac296 | ||
|
|
325d63e1b7 | ||
|
|
e639a77165 | ||
|
|
c075efc752 | ||
|
|
c4f42f71c3 | ||
|
|
535adfe200 | ||
|
|
85fa159f0d | ||
|
|
fd2fe46c95 | ||
|
|
6e52f35626 | ||
|
|
a0d1e7023d | ||
|
|
97a2f00d59 | ||
|
|
50ad4efad7 | ||
|
|
79a3d9c8df | ||
|
|
b8e20d885f |
5
.github/workflows/artifacts.yml
vendored
5
.github/workflows/artifacts.yml
vendored
@@ -7,6 +7,7 @@ permissions: write-all
|
||||
|
||||
jobs:
|
||||
build-artifacts:
|
||||
name: Build
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -37,7 +38,7 @@ jobs:
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('src-tauri/Cargo.lock') }}
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 20
|
||||
cache: 'npm'
|
||||
- name: install dependencies (ubuntu only)
|
||||
if: matrix.os == 'ubuntu-20.04'
|
||||
@@ -65,7 +66,7 @@ jobs:
|
||||
with:
|
||||
tagName: 'v__VERSION__'
|
||||
releaseName: 'Release __VERSION__'
|
||||
releaseBody: '<!-- Release Notes -->'
|
||||
releaseBody: 'https://yaak.app/changelog/__VERSION__'
|
||||
releaseDraft: true
|
||||
prerelease: false
|
||||
args: '--target ${{ matrix.target }}'
|
||||
|
||||
33
src-tauri/Cargo.lock
generated
33
src-tauri/Cargo.lock
generated
@@ -1699,6 +1699,7 @@ dependencies = [
|
||||
"protoc-bin-vendored",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tonic",
|
||||
@@ -1763,9 +1764,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.3.21"
|
||||
version = "0.3.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91fc23aa11be92976ef4729127f1a74adf36d8436f7816b185d18df956790833"
|
||||
checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"fnv",
|
||||
@@ -1773,7 +1774,7 @@ dependencies = [
|
||||
"futures-sink",
|
||||
"futures-util",
|
||||
"http",
|
||||
"indexmap 1.9.3",
|
||||
"indexmap 2.1.0",
|
||||
"slab",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
@@ -2974,6 +2975,16 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "os_pipe"
|
||||
version = "1.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57119c3b893986491ec9aa85056780d3a0f3cf4da7cc09dd3650dbd6c6738fb9"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "overload"
|
||||
version = "0.1.1"
|
||||
@@ -4164,6 +4175,16 @@ dependencies = [
|
||||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shared_child"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b0d94659ad3c2137fef23ae75b03d5241d633f8acded53d672decfa0e6e0caef"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signature"
|
||||
version = "2.1.0"
|
||||
@@ -4197,9 +4218,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.11.2"
|
||||
version = "1.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970"
|
||||
checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7"
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
@@ -4776,6 +4797,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"open",
|
||||
"os_info",
|
||||
"os_pipe",
|
||||
"percent-encoding",
|
||||
"rand 0.8.5",
|
||||
"raw-window-handle",
|
||||
@@ -4787,6 +4809,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"serde_repr",
|
||||
"serialize-to-javascript",
|
||||
"shared_child",
|
||||
"state",
|
||||
"sys-locale",
|
||||
"tar",
|
||||
|
||||
@@ -44,6 +44,7 @@ tauri = { version = "1.5.4", features = [
|
||||
"os-all",
|
||||
"protocol-asset",
|
||||
"shell-open",
|
||||
"shell-sidecar",
|
||||
"updater",
|
||||
"window-close",
|
||||
"window-maximize",
|
||||
|
||||
@@ -20,3 +20,4 @@ hyper = { version = "0.14" }
|
||||
hyper-rustls = { version = "0.24.0", features = ["http2"] }
|
||||
protoc-bin-vendored = "3.0.0"
|
||||
uuid = { version = "1.7.0", features = ["v4"] }
|
||||
tauri = { version = "1.5.4", features = ["process-command-api"]}
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
use std::env::temp_dir;
|
||||
use std::ops::Deref;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use hyper::client::HttpConnector;
|
||||
use hyper::Client;
|
||||
use hyper::client::HttpConnector;
|
||||
use hyper_rustls::{HttpsConnector, HttpsConnectorBuilder};
|
||||
use log::{debug, warn};
|
||||
use log::{debug, info, warn};
|
||||
use prost::Message;
|
||||
use prost_reflect::{DescriptorPool, MethodDescriptor};
|
||||
use prost_types::{FileDescriptorProto, FileDescriptorSet};
|
||||
use tauri::api::process::{Command, CommandEvent};
|
||||
use tokio::fs;
|
||||
use tokio_stream::StreamExt;
|
||||
use tonic::body::BoxBody;
|
||||
use tonic::codegen::http::uri::PathAndQuery;
|
||||
use tonic::transport::Uri;
|
||||
use tonic::Request;
|
||||
use tonic::transport::Uri;
|
||||
use tonic_reflection::pb::server_reflection_client::ServerReflectionClient;
|
||||
use tonic_reflection::pb::server_reflection_request::MessageRequest;
|
||||
use tonic_reflection::pb::server_reflection_response::MessageResponse;
|
||||
@@ -27,36 +27,67 @@ pub async fn fill_pool_from_files(paths: Vec<PathBuf>) -> Result<DescriptorPool,
|
||||
let mut pool = DescriptorPool::new();
|
||||
let random_file_name = format!("{}.desc", uuid::Uuid::new_v4());
|
||||
let desc_path = temp_dir().join(random_file_name);
|
||||
let bin = protoc_bin_vendored::protoc_bin_path().unwrap();
|
||||
|
||||
let mut cmd = Command::new(bin.clone());
|
||||
cmd.arg("--include_imports")
|
||||
.arg("--include_source_info")
|
||||
.arg("-o")
|
||||
.arg(&desc_path);
|
||||
let mut args = vec![
|
||||
"--include_imports".to_string(),
|
||||
"--include_source_info".to_string(),
|
||||
"-o".to_string(),
|
||||
desc_path.to_string_lossy().to_string(),
|
||||
];
|
||||
|
||||
for p in paths {
|
||||
if p.as_path().exists() {
|
||||
cmd.arg(p.as_path().to_string_lossy().as_ref());
|
||||
args.push(p.to_string_lossy().to_string());
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
let parent = p.as_path().parent();
|
||||
if let Some(parent_path) = parent {
|
||||
cmd.arg("-I").arg(parent_path);
|
||||
cmd.arg("-I").arg(parent_path.parent().unwrap());
|
||||
args.push("-I".to_string());
|
||||
args.push(parent_path.to_string_lossy().to_string());
|
||||
args.push("-I".to_string());
|
||||
args.push(parent_path.parent().unwrap().to_string_lossy().to_string());
|
||||
} else {
|
||||
debug!("ignoring {:?} since it does not exist.", parent)
|
||||
}
|
||||
}
|
||||
|
||||
let output = cmd.output().map_err(|e| e.to_string())?;
|
||||
if !output.status.success() {
|
||||
return Err(format!(
|
||||
"protoc failed: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
));
|
||||
let (mut rx, _child) = Command::new_sidecar("protoc")
|
||||
.expect("protoc not found")
|
||||
.args(args)
|
||||
.spawn()
|
||||
.expect("protoc failed to start");
|
||||
|
||||
while let Some(event) = rx.recv().await {
|
||||
match event {
|
||||
CommandEvent::Stdout(line) => {
|
||||
info!("protoc stdout: {}", line);
|
||||
}
|
||||
CommandEvent::Stderr(line) => {
|
||||
info!("protoc stderr: {}", line);
|
||||
}
|
||||
CommandEvent::Error(e) => {
|
||||
return Err(e.to_string());
|
||||
}
|
||||
CommandEvent::Terminated(c) => {
|
||||
match c.code {
|
||||
Some(0) => {
|
||||
// success
|
||||
}
|
||||
Some(code) => {
|
||||
return Err(format!(
|
||||
"protoc failed with exit code: {}",
|
||||
code,
|
||||
));
|
||||
}
|
||||
None => {
|
||||
return Err("protoc failed with no exit code".to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
|
||||
let bytes = fs::read(desc_path.as_path())
|
||||
|
||||
BIN
src-tauri/protoc-vendored/protoc-aarch64-apple-darwin
Executable file
BIN
src-tauri/protoc-vendored/protoc-aarch64-apple-darwin
Executable file
Binary file not shown.
BIN
src-tauri/protoc-vendored/protoc-x86_64-apple-darwin
Executable file
BIN
src-tauri/protoc-vendored/protoc-x86_64-apple-darwin
Executable file
Binary file not shown.
BIN
src-tauri/protoc-vendored/protoc-x86_64-pc-windows-msvc.exe
Executable file
BIN
src-tauri/protoc-vendored/protoc-x86_64-pc-windows-msvc.exe
Executable file
Binary file not shown.
BIN
src-tauri/protoc-vendored/protoc-x86_64-unknown-linux-gnu
Executable file
BIN
src-tauri/protoc-vendored/protoc-x86_64-unknown-linux-gnu
Executable file
Binary file not shown.
@@ -1,11 +1,13 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use log::{debug, warn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use sqlx::types::JsonValue;
|
||||
use tauri::{AppHandle, Manager};
|
||||
|
||||
use crate::{is_dev, models};
|
||||
use crate::is_dev;
|
||||
use crate::models::{generate_id, get_key_value_int, get_key_value_string, set_key_value_int, set_key_value_string};
|
||||
|
||||
// serializable
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
@@ -34,7 +36,11 @@ impl AnalyticsResource {
|
||||
|
||||
impl Display for AnalyticsResource {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", serde_json::to_string(self).unwrap().replace("\"", ""))
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
serde_json::to_string(self).unwrap().replace("\"", "")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +48,7 @@ impl Display for AnalyticsResource {
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AnalyticsAction {
|
||||
Cancel,
|
||||
Commit,
|
||||
Create,
|
||||
Delete,
|
||||
DeleteMany,
|
||||
@@ -67,7 +74,11 @@ impl AnalyticsAction {
|
||||
|
||||
impl Display for AnalyticsAction {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", serde_json::to_string(self).unwrap().replace("\"", ""))
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
serde_json::to_string(self).unwrap().replace("\"", "")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,10 +96,9 @@ pub async fn track_launch_event(app_handle: &AppHandle) -> LaunchEventInfo {
|
||||
|
||||
let mut info = LaunchEventInfo::default();
|
||||
|
||||
info.num_launches =
|
||||
models::get_key_value_int(app_handle, namespace, "num_launches", 0).await + 1;
|
||||
info.num_launches = get_key_value_int(app_handle, namespace, "num_launches", 0).await + 1;
|
||||
info.previous_version =
|
||||
models::get_key_value_string(app_handle, namespace, last_tracked_version_key, "").await;
|
||||
get_key_value_string(app_handle, namespace, last_tracked_version_key, "").await;
|
||||
info.current_version = app_handle.package_info().version.to_string();
|
||||
|
||||
if info.previous_version.is_empty() {
|
||||
@@ -123,14 +133,14 @@ pub async fn track_launch_event(app_handle: &AppHandle) -> LaunchEventInfo {
|
||||
|
||||
// Update key values
|
||||
|
||||
models::set_key_value_string(
|
||||
set_key_value_string(
|
||||
app_handle,
|
||||
namespace,
|
||||
last_tracked_version_key,
|
||||
info.current_version.as_str(),
|
||||
)
|
||||
.await;
|
||||
models::set_key_value_int(app_handle, namespace, "num_launches", info.num_launches).await;
|
||||
set_key_value_int(app_handle, namespace, "num_launches", info.num_launches).await;
|
||||
|
||||
info
|
||||
}
|
||||
@@ -141,6 +151,7 @@ pub async fn track_event(
|
||||
action: AnalyticsAction,
|
||||
attributes: Option<JsonValue>,
|
||||
) {
|
||||
let id = get_id(app_handle).await;
|
||||
let event = format!("{}.{}", resource, action);
|
||||
let attributes_json = attributes.unwrap_or("{}".to_string().into()).to_string();
|
||||
let info = app_handle.package_info();
|
||||
@@ -154,6 +165,7 @@ pub async fn track_event(
|
||||
false => "https://t.yaak.app",
|
||||
};
|
||||
let params = vec![
|
||||
("u", id),
|
||||
("e", event.clone()),
|
||||
("a", attributes_json.clone()),
|
||||
("id", site.to_string()),
|
||||
@@ -170,7 +182,7 @@ pub async fn track_event(
|
||||
|
||||
// Disable analytics actual sending in dev
|
||||
if is_dev() {
|
||||
debug!("track: {} {}", event, attributes_json);
|
||||
debug!("track: {} {} {:?}", event, attributes_json, params);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -216,3 +228,14 @@ fn get_window_size(app_handle: &AppHandle) -> String {
|
||||
(height / 100.0).round() * 100.0
|
||||
)
|
||||
}
|
||||
|
||||
async fn get_id(app_handle: &AppHandle) -> String {
|
||||
let id = get_key_value_string(app_handle, "analytics", "id", "").await;
|
||||
if id.is_empty() {
|
||||
let new_id = generate_id(None);
|
||||
set_key_value_string(app_handle, "analytics", "id", new_id.as_str()).await;
|
||||
new_id
|
||||
} else {
|
||||
id
|
||||
}
|
||||
}
|
||||
|
||||
@@ -828,11 +828,14 @@ async fn cmd_send_http_request(
|
||||
.expect("Failed to get request");
|
||||
|
||||
let environment = match environment_id {
|
||||
Some(id) => Some(
|
||||
get_environment(&window, id)
|
||||
.await
|
||||
.expect("Failed to get environment"),
|
||||
),
|
||||
Some(id) =>
|
||||
match get_environment(&window, id).await {
|
||||
Ok(env) => Some(env),
|
||||
Err(e) => {
|
||||
warn!("Failed to find environment by id {id} {}", e);
|
||||
None
|
||||
}
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
|
||||
@@ -1386,7 +1389,11 @@ fn main() {
|
||||
.level_for("tower", log::LevelFilter::Info)
|
||||
.level_for("tracing", log::LevelFilter::Info)
|
||||
.with_colors(ColoredLevelConfig::default())
|
||||
.level(log::LevelFilter::Trace)
|
||||
.level(if is_dev() {
|
||||
log::LevelFilter::Trace
|
||||
} else {
|
||||
log::LevelFilter::Info
|
||||
})
|
||||
.build(),
|
||||
)
|
||||
.setup(|app| {
|
||||
@@ -1530,7 +1537,7 @@ fn main() {
|
||||
let h = app_handle.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let info = analytics::track_launch_event(&h).await;
|
||||
info!("Launched Yaak {:?}", info);
|
||||
debug!("Launched Yaak {:?}", info);
|
||||
|
||||
// Wait for window render and give a chance for the user to notice
|
||||
if info.launched_after_update && info.num_launches > 1 {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
},
|
||||
"package": {
|
||||
"productName": "Yaak",
|
||||
"version": "2024.3.0-beta.1"
|
||||
"version": "2024.3.0"
|
||||
},
|
||||
"tauri": {
|
||||
"windows": [],
|
||||
@@ -32,7 +32,13 @@
|
||||
},
|
||||
"shell": {
|
||||
"all": false,
|
||||
"open": true
|
||||
"open": true,
|
||||
"sidecar": true,
|
||||
"scope": [
|
||||
{ "name": "protoc", "sidecar": true,
|
||||
"args": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"window": {
|
||||
"close": true,
|
||||
@@ -56,7 +62,9 @@
|
||||
"active": true,
|
||||
"category": "DeveloperTool",
|
||||
"copyright": "",
|
||||
"externalBin": [],
|
||||
"externalBin": [
|
||||
"protoc-vendored/protoc"
|
||||
],
|
||||
"icon": [
|
||||
"icons/release/32x32.png",
|
||||
"icons/release/128x128.png",
|
||||
|
||||
@@ -1,53 +1,17 @@
|
||||
import { useCreateFolder } from '../hooks/useCreateFolder';
|
||||
import { useCreateGrpcRequest } from '../hooks/useCreateGrpcRequest';
|
||||
import { useCreateHttpRequest } from '../hooks/useCreateHttpRequest';
|
||||
import { BODY_TYPE_GRAPHQL } from '../lib/models';
|
||||
import type { DropdownItem, DropdownProps } from './core/Dropdown';
|
||||
import { useCreateDropdownItems } from '../hooks/useCreateDropdownItems';
|
||||
import type { DropdownProps } from './core/Dropdown';
|
||||
import { Dropdown } from './core/Dropdown';
|
||||
|
||||
interface Props {
|
||||
hideFolder?: boolean;
|
||||
children: DropdownProps['children'];
|
||||
openOnHotKeyAction?: DropdownProps['openOnHotKeyAction'];
|
||||
}
|
||||
|
||||
export function CreateDropdown({ hideFolder, children }: Props) {
|
||||
const createHttpRequest = useCreateHttpRequest();
|
||||
const createGrpcRequest = useCreateGrpcRequest();
|
||||
const createFolder = useCreateFolder();
|
||||
|
||||
export function CreateDropdown({ hideFolder, children, openOnHotKeyAction }: Props) {
|
||||
const items = useCreateDropdownItems({ hideFolder, hideIcons: true });
|
||||
return (
|
||||
<Dropdown
|
||||
openOnHotKeyAction="http_request.create"
|
||||
items={[
|
||||
{
|
||||
key: 'create-http-request',
|
||||
label: 'HTTP Request',
|
||||
onSelect: () => createHttpRequest.mutate({}),
|
||||
},
|
||||
{
|
||||
key: 'create-graphql-request',
|
||||
label: 'GraphQL Query',
|
||||
onSelect: () => createHttpRequest.mutate({ bodyType: BODY_TYPE_GRAPHQL, method: 'POST' }),
|
||||
},
|
||||
{
|
||||
key: 'create-grpc-request',
|
||||
label: 'gRPC Call',
|
||||
onSelect: () => createGrpcRequest.mutate({}),
|
||||
},
|
||||
...((hideFolder
|
||||
? []
|
||||
: [
|
||||
{
|
||||
type: 'separator',
|
||||
},
|
||||
{
|
||||
key: 'create-folder',
|
||||
label: 'Folder',
|
||||
onSelect: () => createFolder.mutate({}),
|
||||
},
|
||||
]) as DropdownItem[]),
|
||||
]}
|
||||
>
|
||||
<Dropdown openOnHotKeyAction={openOnHotKeyAction} items={items}>
|
||||
{children}
|
||||
</Dropdown>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { createContext, useContext, useMemo, useState } from 'react';
|
||||
import { useHotKey } from '../hooks/useHotKey';
|
||||
import { trackEvent } from '../lib/analytics';
|
||||
import type { DialogProps } from './core/Dialog';
|
||||
import { Dialog } from './core/Dialog';
|
||||
@@ -21,7 +20,7 @@ interface Actions {
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const DialogContext = createContext<State>({} as any);
|
||||
const DialogContext = createContext<State>({} as State);
|
||||
|
||||
export const DialogProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const [dialogs, setDialogs] = useState<State['dialogs']>([]);
|
||||
@@ -60,7 +59,6 @@ export const DialogProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
function DialogInstance({ id, render, ...props }: DialogEntry) {
|
||||
const { actions } = useContext(DialogContext);
|
||||
const children = render({ hide: () => actions.hide(id) });
|
||||
useHotKey('popup.close', () => actions.hide(id));
|
||||
return (
|
||||
<Dialog open onClose={() => actions.hide(id)} {...props}>
|
||||
{children}
|
||||
|
||||
@@ -119,6 +119,11 @@ export function GrpcConnectionSetupPane({
|
||||
onGo();
|
||||
}, [activeRequest, onGo]);
|
||||
|
||||
const handleSend = useCallback(async () => {
|
||||
if (activeRequest == null) return;
|
||||
onSend({ message: activeRequest.message });
|
||||
}, [activeRequest, onGo]);
|
||||
|
||||
const tabs: TabItem[] = useMemo(
|
||||
() => [
|
||||
{ value: 'message', label: 'Message' },
|
||||
@@ -212,52 +217,52 @@ export function GrpcConnectionSetupPane({
|
||||
{select.options.find((o) => o.value === select.value)?.label ?? 'No Schema'}
|
||||
</Button>
|
||||
</RadioDropdown>
|
||||
{!isStreaming && (
|
||||
{methodType === 'client_streaming' || methodType === 'streaming' ? (
|
||||
<>
|
||||
{isStreaming && (
|
||||
<>
|
||||
<IconButton
|
||||
className="border border-highlight"
|
||||
size="sm"
|
||||
title="Cancel"
|
||||
onClick={onCancel}
|
||||
icon="x"
|
||||
/>
|
||||
<IconButton
|
||||
className="border border-highlight"
|
||||
size="sm"
|
||||
title="Commit"
|
||||
onClick={onCommit}
|
||||
icon="check"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<IconButton
|
||||
className="border border-highlight"
|
||||
size="sm"
|
||||
title={isStreaming ? 'Connect' : 'Send'}
|
||||
hotkeyAction="grpc_request.send"
|
||||
onClick={isStreaming ? handleSend : handleConnect}
|
||||
icon={isStreaming ? 'sendHorizontal' : 'arrowUpDown'}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<IconButton
|
||||
className="border border-highlight"
|
||||
size="sm"
|
||||
title={methodType === 'unary' ? 'Send' : 'Connect'}
|
||||
hotkeyAction="grpc_request.send"
|
||||
onClick={handleConnect}
|
||||
onClick={isStreaming ? onCancel : handleConnect}
|
||||
disabled={methodType === 'no-schema' || methodType === 'no-method'}
|
||||
icon={
|
||||
isStreaming
|
||||
? 'refresh'
|
||||
? 'x'
|
||||
: methodType.includes('streaming')
|
||||
? 'arrowUpDown'
|
||||
: 'sendHorizontal'
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{isStreaming && (
|
||||
<IconButton
|
||||
className="border border-highlight"
|
||||
size="sm"
|
||||
title="Cancel"
|
||||
onClick={onCancel}
|
||||
icon="x"
|
||||
disabled={!isStreaming}
|
||||
/>
|
||||
)}
|
||||
{(methodType === 'client_streaming' || methodType === 'streaming') && isStreaming && (
|
||||
<>
|
||||
<IconButton
|
||||
className="border border-highlight"
|
||||
size="sm"
|
||||
title="to-do"
|
||||
onClick={onCommit}
|
||||
icon="check"
|
||||
/>
|
||||
<IconButton
|
||||
className="border border-highlight"
|
||||
size="sm"
|
||||
title="to-do"
|
||||
hotkeyAction="grpc_request.send"
|
||||
onClick={() => onSend({ message: activeRequest.message ?? '' })}
|
||||
icon="sendHorizontal"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</HStack>
|
||||
</div>
|
||||
<Tabs
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import classNames from 'classnames';
|
||||
import { useMemo, useRef } from 'react';
|
||||
import { useKey, useKeyPressEvent } from 'react-use';
|
||||
import { useActiveEnvironmentId } from '../hooks/useActiveEnvironmentId';
|
||||
import { useActiveEnvironment } from '../hooks/useActiveEnvironment';
|
||||
import { useActiveRequest } from '../hooks/useActiveRequest';
|
||||
import { useActiveWorkspaceId } from '../hooks/useActiveWorkspaceId';
|
||||
import { useAppRoutes } from '../hooks/useAppRoutes';
|
||||
@@ -14,12 +14,13 @@ import type { ButtonProps } from './core/Button';
|
||||
import { Button } from './core/Button';
|
||||
import type { DropdownItem, DropdownRef } from './core/Dropdown';
|
||||
import { Dropdown } from './core/Dropdown';
|
||||
import { HttpMethodTag } from './core/HttpMethodTag';
|
||||
|
||||
export function RecentRequestsDropdown({ className }: Pick<ButtonProps, 'className'>) {
|
||||
const dropdownRef = useRef<DropdownRef>(null);
|
||||
const activeRequest = useActiveRequest();
|
||||
const activeWorkspaceId = useActiveWorkspaceId();
|
||||
const activeEnvironmentId = useActiveEnvironmentId();
|
||||
const activeEnvironment = useActiveEnvironment();
|
||||
const httpRequests = useHttpRequests();
|
||||
const grpcRequests = useGrpcRequests();
|
||||
const routes = useAppRoutes();
|
||||
@@ -63,10 +64,11 @@ export function RecentRequestsDropdown({ className }: Pick<ButtonProps, 'classNa
|
||||
key: request.id,
|
||||
label: fallbackRequestName(request),
|
||||
// leftSlot: <CountBadge className="!ml-0 px-0 w-5" count={recentRequestItems.length} />,
|
||||
leftSlot: <HttpMethodTag request={request} />,
|
||||
onSelect: () => {
|
||||
routes.navigate('request', {
|
||||
requestId: request.id,
|
||||
environmentId: activeEnvironmentId ?? undefined,
|
||||
environmentId: activeEnvironment?.id,
|
||||
workspaceId: activeWorkspaceId,
|
||||
});
|
||||
},
|
||||
@@ -85,7 +87,7 @@ export function RecentRequestsDropdown({ className }: Pick<ButtonProps, 'classNa
|
||||
}
|
||||
|
||||
return recentRequestItems.slice(0, 20);
|
||||
}, [activeWorkspaceId, activeEnvironmentId, recentRequestIds, requests, routes]);
|
||||
}, [activeWorkspaceId, activeEnvironment?.id, recentRequestIds, requests, routes]);
|
||||
|
||||
return (
|
||||
<Dropdown ref={dropdownRef} items={items}>
|
||||
|
||||
@@ -9,10 +9,7 @@ import { useActiveEnvironmentId } from '../hooks/useActiveEnvironmentId';
|
||||
import { useActiveRequest } from '../hooks/useActiveRequest';
|
||||
import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
|
||||
import { useAppRoutes } from '../hooks/useAppRoutes';
|
||||
import { useCreateFolder } from '../hooks/useCreateFolder';
|
||||
import { useCreateHttpRequest } from '../hooks/useCreateHttpRequest';
|
||||
import { useDeleteAnyGrpcRequest } from '../hooks/useDeleteAnyGrpcRequest';
|
||||
import { useDeleteAnyHttpRequest } from '../hooks/useDeleteAnyHttpRequest';
|
||||
import { useCreateDropdownItems } from '../hooks/useCreateDropdownItems';
|
||||
import { useDeleteFolder } from '../hooks/useDeleteFolder';
|
||||
import { useDeleteRequest } from '../hooks/useDeleteRequest';
|
||||
import { useDuplicateGrpcRequest } from '../hooks/useDuplicateGrpcRequest';
|
||||
@@ -61,15 +58,13 @@ interface TreeNode {
|
||||
}
|
||||
|
||||
export function Sidebar({ className }: Props) {
|
||||
const { hidden } = useSidebarHidden();
|
||||
const { hidden, show, hide } = useSidebarHidden();
|
||||
const sidebarRef = useRef<HTMLLIElement>(null);
|
||||
const activeRequest = useActiveRequest();
|
||||
const activeEnvironmentId = useActiveEnvironmentId();
|
||||
const httpRequests = useHttpRequests();
|
||||
const grpcRequests = useGrpcRequests();
|
||||
const folders = useFolders();
|
||||
const deleteAnyHttpRequest = useDeleteAnyHttpRequest();
|
||||
const deleteAnyGrpcRequest = useDeleteAnyGrpcRequest();
|
||||
const activeWorkspace = useActiveWorkspace();
|
||||
const duplicateHttpRequest = useDuplicateHttpRequest({
|
||||
id: activeRequest?.id ?? null,
|
||||
@@ -91,7 +86,7 @@ export function Sidebar({ className }: Props) {
|
||||
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
|
||||
const collapsed = useKeyValue<Record<string, boolean>>({
|
||||
key: ['sidebar_collapsed', activeWorkspace?.id ?? 'n/a'],
|
||||
defaultValue: {},
|
||||
fallback: {},
|
||||
namespace: NAMESPACE_NO_SYNC,
|
||||
});
|
||||
|
||||
@@ -108,9 +103,10 @@ export function Sidebar({ className }: Props) {
|
||||
[collapsed.value],
|
||||
);
|
||||
|
||||
const { tree, treeParentMap, selectableRequests } = useMemo<{
|
||||
const { tree, treeParentMap, selectableRequests, selectedRequest } = useMemo<{
|
||||
tree: TreeNode | null;
|
||||
treeParentMap: Record<string, TreeNode>;
|
||||
selectedRequest: HttpRequest | GrpcRequest | null;
|
||||
selectableRequests: {
|
||||
id: string;
|
||||
index: number;
|
||||
@@ -123,14 +119,23 @@ export function Sidebar({ className }: Props) {
|
||||
index: number;
|
||||
tree: TreeNode;
|
||||
}[] = [];
|
||||
|
||||
if (activeWorkspace == null) {
|
||||
return { tree: null, treeParentMap, selectableRequests };
|
||||
return { tree: null, treeParentMap, selectableRequests, selectedRequest: null };
|
||||
}
|
||||
|
||||
let selectedRequest: HttpRequest | GrpcRequest | null = null;
|
||||
let selectableRequestIndex = 0;
|
||||
|
||||
// Put requests and folders into a tree structure
|
||||
const next = (node: TreeNode): TreeNode => {
|
||||
if (
|
||||
node.item.id === selectedId &&
|
||||
(node.item.model === 'http_request' || node.item.model === 'grpc_request')
|
||||
) {
|
||||
selectedRequest = node.item;
|
||||
}
|
||||
|
||||
const childItems = [...httpRequests, ...grpcRequests, ...folders].filter((f) =>
|
||||
node.item.model === 'workspace' ? f.folderId == null : f.folderId === node.item.id,
|
||||
);
|
||||
@@ -149,8 +154,10 @@ export function Sidebar({ className }: Props) {
|
||||
|
||||
const tree = next({ item: activeWorkspace, children: [], depth: 0 });
|
||||
|
||||
return { tree, treeParentMap, selectableRequests };
|
||||
}, [activeWorkspace, httpRequests, grpcRequests, folders]);
|
||||
return { tree, treeParentMap, selectableRequests, selectedRequest };
|
||||
}, [activeWorkspace, selectedId, httpRequests, grpcRequests, folders]);
|
||||
|
||||
const deleteSelectedRequest = useDeleteRequest(selectedRequest);
|
||||
|
||||
const focusActiveRequest = useCallback(
|
||||
(
|
||||
@@ -167,13 +174,14 @@ export function Sidebar({ className }: Props) {
|
||||
const children = tree?.children ?? [];
|
||||
const id =
|
||||
forced?.id ?? children.find((m) => m.item.id === activeRequest?.id)?.item.id ?? null;
|
||||
|
||||
setHasFocus(true);
|
||||
setSelectedId(id);
|
||||
setSelectedTree(tree);
|
||||
|
||||
if (id == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedId(id);
|
||||
setSelectedTree(tree);
|
||||
setHasFocus(true);
|
||||
if (!noFocusSidebar) {
|
||||
sidebarRef.current?.focus();
|
||||
}
|
||||
@@ -221,23 +229,33 @@ export function Sidebar({ className }: Props) {
|
||||
const handleBlur = useCallback(() => setHasFocus(false), []);
|
||||
|
||||
const handleDeleteKey = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
async (e: KeyboardEvent) => {
|
||||
if (!hasFocus) return;
|
||||
e.preventDefault();
|
||||
|
||||
const selected = selectableRequests.find((r) => r.id === selectedId);
|
||||
if (selected == null) return;
|
||||
deleteAnyHttpRequest.mutate(selected.id);
|
||||
deleteAnyGrpcRequest.mutate(selected.id);
|
||||
await deleteSelectedRequest.mutateAsync();
|
||||
},
|
||||
[deleteAnyHttpRequest, deleteAnyGrpcRequest, hasFocus, selectableRequests, selectedId],
|
||||
[hasFocus, selectableRequests, deleteSelectedRequest, selectedId],
|
||||
);
|
||||
|
||||
useKeyPressEvent('Backspace', handleDeleteKey);
|
||||
useKeyPressEvent('Delete', handleDeleteKey);
|
||||
|
||||
useHotKey('sidebar.focus', () => {
|
||||
if (hidden || hasFocus) return;
|
||||
useHotKey('sidebar.focus', async () => {
|
||||
console.log('sidebar.focus', { hidden, hasFocus });
|
||||
// Hide the sidebar if it's already focused
|
||||
if (!hidden && hasFocus) {
|
||||
await hide();
|
||||
return;
|
||||
}
|
||||
|
||||
// Show the sidebar if it's hidden
|
||||
if (hidden) {
|
||||
await show();
|
||||
}
|
||||
|
||||
// Select 0 index on focus if none selected
|
||||
focusActiveRequest(
|
||||
selectedTree != null && selectedId != null
|
||||
@@ -496,13 +514,9 @@ function SidebarItems({
|
||||
}
|
||||
itemModel={child.item.model}
|
||||
itemPrefix={
|
||||
child.item.model === 'http_request' && child.item.bodyType === 'graphql' ? (
|
||||
<HttpMethodTag className="opacity-50">GQL</HttpMethodTag>
|
||||
) : child.item.model === 'http_request' ? (
|
||||
<HttpMethodTag className="opacity-50">{child.item.method}</HttpMethodTag>
|
||||
) : child.item.model === 'grpc_request' ? (
|
||||
<HttpMethodTag className="opacity-50">GRPC</HttpMethodTag>
|
||||
) : null
|
||||
(child.item.model === 'http_request' || child.item.model === 'grpc_request') && (
|
||||
<HttpMethodTag request={child.item} />
|
||||
)
|
||||
}
|
||||
onMove={handleMove}
|
||||
onEnd={handleEnd}
|
||||
@@ -574,10 +588,8 @@ const SidebarItem = forwardRef(function SidebarItem(
|
||||
ref: ForwardedRef<HTMLLIElement>,
|
||||
) {
|
||||
const activeRequest = useActiveRequest();
|
||||
const createRequest = useCreateHttpRequest();
|
||||
const createFolder = useCreateFolder();
|
||||
const deleteFolder = useDeleteFolder(itemId);
|
||||
const deleteRequest = useDeleteRequest(itemId);
|
||||
const deleteRequest = useDeleteRequest(activeRequest ?? null);
|
||||
const duplicateHttpRequest = useDuplicateHttpRequest({ id: itemId, navigateAfter: true });
|
||||
const duplicateGrpcRequest = useDuplicateGrpcRequest({ id: itemId, navigateAfter: true });
|
||||
const sendRequest = useSendRequest(itemId);
|
||||
@@ -590,6 +602,7 @@ const SidebarItem = forwardRef(function SidebarItem(
|
||||
const prompt = usePrompt();
|
||||
const [editing, setEditing] = useState<boolean>(false);
|
||||
const isActive = activeRequest?.id === itemId;
|
||||
const createDropdownItems = useCreateDropdownItems({ folderId: itemId });
|
||||
|
||||
const handleSubmitNameEdit = useCallback(
|
||||
(el: HTMLInputElement) => {
|
||||
@@ -693,18 +706,7 @@ const SidebarItem = forwardRef(function SidebarItem(
|
||||
onSelect: () => deleteFolder.mutate(),
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
key: 'createRequest',
|
||||
label: 'New Request',
|
||||
leftSlot: <Icon icon="plus" />,
|
||||
onSelect: () => createRequest.mutate({ folderId: itemId }),
|
||||
},
|
||||
{
|
||||
key: 'createFolder',
|
||||
label: 'New Folder',
|
||||
leftSlot: <Icon icon="plus" />,
|
||||
onSelect: () => createFolder.mutate({ folderId: itemId, sortPriority: -1 }),
|
||||
},
|
||||
...createDropdownItems,
|
||||
]
|
||||
: [
|
||||
...((itemModel === 'http_request'
|
||||
|
||||
@@ -25,7 +25,7 @@ export const SidebarActions = memo(function SidebarActions() {
|
||||
hotkeyAction="sidebar.toggle"
|
||||
icon={hidden ? 'leftPanelHidden' : 'leftPanelVisible'}
|
||||
/>
|
||||
<CreateDropdown>
|
||||
<CreateDropdown openOnHotKeyAction="http_request.create">
|
||||
<IconButton size="sm" icon="plusCircle" title="Add Resource" />
|
||||
</CreateDropdown>
|
||||
</HStack>
|
||||
|
||||
@@ -148,18 +148,7 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
|
||||
key: 'create-workspace',
|
||||
label: 'New Workspace',
|
||||
leftSlot: <Icon icon="plus" />,
|
||||
onSelect: async () => {
|
||||
const name = await prompt({
|
||||
id: 'new-workspace',
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
defaultValue: 'My Workspace',
|
||||
title: 'New Workspace',
|
||||
confirmLabel: 'Create',
|
||||
placeholder: 'My Workspace',
|
||||
});
|
||||
createWorkspace.mutate({ name });
|
||||
},
|
||||
onSelect: () => createWorkspace.mutate({}),
|
||||
},
|
||||
];
|
||||
}, [
|
||||
|
||||
@@ -2,7 +2,7 @@ import classNames from 'classnames';
|
||||
import { motion } from 'framer-motion';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { useHotKey } from '../../hooks/useHotKey';
|
||||
import { useKey } from 'react-use';
|
||||
import { Overlay } from '../Overlay';
|
||||
import { Heading } from './Heading';
|
||||
import { IconButton } from './IconButton';
|
||||
@@ -36,7 +36,15 @@ export function Dialog({
|
||||
[description],
|
||||
);
|
||||
|
||||
useHotKey('popup.close', onClose);
|
||||
useKey(
|
||||
'Escape',
|
||||
() => {
|
||||
if (!open) return;
|
||||
onClose();
|
||||
},
|
||||
{},
|
||||
[open],
|
||||
);
|
||||
|
||||
return (
|
||||
<Overlay open={open} onClose={onClose} portalName="dialog">
|
||||
|
||||
@@ -244,13 +244,16 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle'>, MenuPro
|
||||
}
|
||||
};
|
||||
|
||||
useHotKey('popup.close', () => {
|
||||
if (filter !== '') {
|
||||
setFilter('');
|
||||
} else {
|
||||
handleClose();
|
||||
}
|
||||
});
|
||||
useKey(
|
||||
'Escape',
|
||||
() => {
|
||||
if (!isOpen) return;
|
||||
if (filter !== '') setFilter('');
|
||||
else handleClose();
|
||||
},
|
||||
{},
|
||||
[isOpen, filter, setFilter, handleClose],
|
||||
);
|
||||
|
||||
const handlePrev = useCallback(() => {
|
||||
setSelectedIndex((currIndex) => {
|
||||
@@ -287,11 +290,13 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle'>, MenuPro
|
||||
}, [items]);
|
||||
|
||||
useKey('ArrowUp', (e) => {
|
||||
if (!isOpen) return;
|
||||
e.preventDefault();
|
||||
handlePrev();
|
||||
});
|
||||
|
||||
useKey('ArrowDown', (e) => {
|
||||
if (!isOpen) return;
|
||||
e.preventDefault();
|
||||
handleNext();
|
||||
});
|
||||
@@ -409,7 +414,6 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle'>, MenuPro
|
||||
)}
|
||||
{containerStyles && (
|
||||
<VStack
|
||||
space={0.5}
|
||||
ref={initMenu}
|
||||
style={menuStyles}
|
||||
className={classNames(
|
||||
|
||||
@@ -4,11 +4,6 @@
|
||||
.cm-editor {
|
||||
@apply w-full block text-base;
|
||||
|
||||
* {
|
||||
@apply cursor-text;
|
||||
@apply caret-transparent !important;
|
||||
}
|
||||
|
||||
.cm-cursor {
|
||||
@apply border-gray-800 !important;
|
||||
}
|
||||
@@ -32,17 +27,25 @@
|
||||
.cm-scroller {
|
||||
/* Inherit line-height from outside */
|
||||
line-height: inherit;
|
||||
|
||||
* {
|
||||
@apply cursor-text;
|
||||
@apply caret-transparent !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Don't show selection on blurred input */
|
||||
|
||||
.cm-selectionBackground {
|
||||
@apply bg-transparent;
|
||||
}
|
||||
|
||||
&.cm-focused .cm-selectionBackground {
|
||||
@apply bg-selection;
|
||||
}
|
||||
|
||||
/* Style gutters */
|
||||
|
||||
.cm-gutters {
|
||||
@apply border-0 text-gray-500/50;
|
||||
|
||||
@@ -100,10 +103,10 @@
|
||||
@apply font-mono text-[0.75rem];
|
||||
|
||||
/*
|
||||
* Round corners or they'll stick out of the editor bounds of editor is rounded.
|
||||
* Could potentially be pushed up from the editor like we do with bg color but this
|
||||
* is probably fine.
|
||||
*/
|
||||
* Round corners or they'll stick out of the editor bounds of editor is rounded.
|
||||
* Could potentially be pushed up from the editor like we do with bg color but this
|
||||
* is probably fine.
|
||||
*/
|
||||
@apply rounded-lg;
|
||||
}
|
||||
}
|
||||
@@ -164,8 +167,9 @@
|
||||
@apply h-full flex items-center;
|
||||
|
||||
/* Break characters on line wrapping mode, useful for URL field.
|
||||
* We can make this dynamic if we need it to be configurable later
|
||||
*/
|
||||
* We can make this dynamic if we need it to be configurable later
|
||||
*/
|
||||
|
||||
&.cm-lineWrapping {
|
||||
@apply break-all;
|
||||
}
|
||||
@@ -176,6 +180,62 @@
|
||||
.cm-tooltip.cm-tooltip {
|
||||
@apply shadow-lg bg-gray-50 rounded text-gray-700 border border-gray-200 z-50 pointer-events-auto text-[0.75rem];
|
||||
|
||||
.cm-completionIcon {
|
||||
@apply italic font-mono;
|
||||
|
||||
&::after {
|
||||
content: 'x' !important; /* Default (eg. for GraphQL) */
|
||||
}
|
||||
|
||||
&.cm-completionIcon-class::after {
|
||||
content: 'o' !important;
|
||||
}
|
||||
|
||||
&.cm-completionIcon-constant::after {
|
||||
content: 'c' !important;
|
||||
}
|
||||
|
||||
&.cm-completionIcon-enum::after {
|
||||
content: 'e' !important;
|
||||
}
|
||||
|
||||
&.cm-completionIcon-function::after {
|
||||
content: 'z' !important;
|
||||
}
|
||||
|
||||
&.cm-completionIcon-interface::after {
|
||||
content: 'i' !important;
|
||||
}
|
||||
|
||||
&.cm-completionIcon-keyword::after {
|
||||
content: 'k' !important;
|
||||
}
|
||||
|
||||
&.cm-completionIcon-method::after {
|
||||
content: 'm' !important;
|
||||
}
|
||||
|
||||
&.cm-completionIcon-namespace::after {
|
||||
content: 'n' !important;
|
||||
}
|
||||
|
||||
&.cm-completionIcon-property::after {
|
||||
content: 'a' !important;
|
||||
}
|
||||
|
||||
&.cm-completionIcon-text::after {
|
||||
content: 'x' !important;
|
||||
}
|
||||
|
||||
&.cm-completionIcon-type::after {
|
||||
content: 't' !important;
|
||||
}
|
||||
|
||||
&.cm-completionIcon-variable::after {
|
||||
content: 'x' !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.cm-completionInfo-right {
|
||||
@apply ml-1 -mt-0.5 text-sm;
|
||||
}
|
||||
@@ -202,7 +262,7 @@
|
||||
}
|
||||
|
||||
.cm-completionIcon {
|
||||
@apply text-sm flex items-center pb-0.5 mr-2 flex-shrink-0;
|
||||
@apply text-xs flex items-center pb-0.5 flex-shrink-0;
|
||||
}
|
||||
|
||||
.cm-completionLabel {
|
||||
@@ -216,7 +276,7 @@
|
||||
}
|
||||
|
||||
.cm-editor .cm-panels {
|
||||
@apply bg-transparent border-0 text-gray-800 z-50;
|
||||
@apply bg-gray-100 backdrop-blur-sm p-1 mb-1 text-gray-800 z-20 rounded-md;
|
||||
|
||||
input,
|
||||
button {
|
||||
@@ -224,20 +284,24 @@
|
||||
}
|
||||
|
||||
button {
|
||||
@apply appearance-none bg-none bg-gray-800 text-gray-100 focus:bg-gray-900 cursor-default;
|
||||
@apply appearance-none bg-none bg-gray-200 hocus:bg-gray-300 hocus:text-gray-950 border-0 text-gray-800 cursor-default;
|
||||
}
|
||||
|
||||
button[name='close'] {
|
||||
@apply text-gray-600 hocus:text-gray-900 px-2 -mr-1.5 !important;
|
||||
}
|
||||
|
||||
input {
|
||||
@apply bg-gray-50 border border-highlight focus:border-focus outline-none;
|
||||
@apply bg-gray-50 border border-gray-500/50 focus:border-focus outline-none;
|
||||
}
|
||||
|
||||
label {
|
||||
@apply focus-within:text-gray-950;
|
||||
}
|
||||
|
||||
/* Hide the "All" button */
|
||||
|
||||
button[name='select'] {
|
||||
@apply hidden;
|
||||
}
|
||||
}
|
||||
|
||||
/* Add default icon. Needs low priority so it can be overwritten */
|
||||
.cm-completionIcon::after {
|
||||
content: '𝑥';
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
} from '@codemirror/language';
|
||||
import { lintKeymap } from '@codemirror/lint';
|
||||
|
||||
import { highlightSelectionMatches, searchKeymap } from '@codemirror/search';
|
||||
import { searchKeymap } from '@codemirror/search';
|
||||
import { EditorState } from '@codemirror/state';
|
||||
import {
|
||||
crosshairCursor,
|
||||
@@ -126,7 +126,7 @@ export const baseExtensions = [
|
||||
// debouncedAutocompletionDisplay({ millis: 1000 }),
|
||||
// autocompletion({ closeOnBlur: true, interactionDelay: 200, activateOnTyping: false }),
|
||||
autocompletion({
|
||||
// closeOnBlur: false,
|
||||
closeOnBlur: false, // For debugging in devtools without closing it
|
||||
compareCompletions: (a, b) => {
|
||||
// Don't sort completions at all, only on boost
|
||||
return (a.boost ?? 0) - (b.boost ?? 0);
|
||||
@@ -156,7 +156,6 @@ export const multiLineExtensions = [
|
||||
rectangularSelection(),
|
||||
crosshairCursor(),
|
||||
highlightActiveLineGutter(),
|
||||
highlightSelectionMatches({ minSelectionLength: 2 }),
|
||||
keymap.of([
|
||||
indentWithTab,
|
||||
...closeBracketsKeymap,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import classNames from 'classnames';
|
||||
import type { GrpcRequest, HttpRequest } from '../../lib/models';
|
||||
|
||||
interface Props {
|
||||
children: string;
|
||||
request: HttpRequest | GrpcRequest;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
@@ -16,10 +17,17 @@ const methodMap: Record<string, string> = {
|
||||
grpc: 'GRPC',
|
||||
};
|
||||
|
||||
export function HttpMethodTag({ children: method, className }: Props) {
|
||||
export function HttpMethodTag({ request, className }: Props) {
|
||||
const method =
|
||||
request.model === 'http_request' && request.bodyType === 'graphql'
|
||||
? 'GQL'
|
||||
: request.model === 'grpc_request'
|
||||
? 'GRPC'
|
||||
: request.method;
|
||||
|
||||
const m = method.toLowerCase();
|
||||
return (
|
||||
<span className={classNames(className, 'text-2xs font-mono')}>
|
||||
<span className={classNames(className, 'text-2xs font-mono opacity-50')}>
|
||||
{methodMap[m] ?? m.slice(0, 3).toUpperCase()}
|
||||
</span>
|
||||
);
|
||||
|
||||
@@ -11,7 +11,7 @@ export function useActiveCookieJar() {
|
||||
const kv = useKeyValue<string | null>({
|
||||
namespace: NAMESPACE_GLOBAL,
|
||||
key: ['activeCookieJar', workspaceId ?? 'n/a'],
|
||||
defaultValue: null,
|
||||
fallback: null,
|
||||
});
|
||||
|
||||
const activeCookieJar = cookieJars.find((cookieJar) => cookieJar.id === kv.value);
|
||||
|
||||
59
src-web/hooks/useCreateDropdownItems.tsx
Normal file
59
src-web/hooks/useCreateDropdownItems.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import { useMemo } from 'react';
|
||||
import type { DropdownItem } from '../components/core/Dropdown';
|
||||
import { Icon } from '../components/core/Icon';
|
||||
import { BODY_TYPE_GRAPHQL } from '../lib/models';
|
||||
import { useCreateFolder } from './useCreateFolder';
|
||||
import { useCreateGrpcRequest } from './useCreateGrpcRequest';
|
||||
import { useCreateHttpRequest } from './useCreateHttpRequest';
|
||||
|
||||
export function useCreateDropdownItems({
|
||||
hideFolder,
|
||||
hideIcons,
|
||||
folderId,
|
||||
}: {
|
||||
hideFolder?: boolean;
|
||||
hideIcons?: boolean;
|
||||
folderId?: string;
|
||||
} = {}): DropdownItem[] {
|
||||
const createHttpRequest = useCreateHttpRequest();
|
||||
const createGrpcRequest = useCreateGrpcRequest();
|
||||
const createFolder = useCreateFolder();
|
||||
|
||||
return useMemo<DropdownItem[]>(
|
||||
() => [
|
||||
{
|
||||
key: 'create-http-request',
|
||||
label: 'HTTP Request',
|
||||
leftSlot: hideIcons ? undefined : <Icon icon="plus" />,
|
||||
onSelect: () => createHttpRequest.mutate({ folderId }),
|
||||
},
|
||||
{
|
||||
key: 'create-graphql-request',
|
||||
label: 'GraphQL Query',
|
||||
leftSlot: hideIcons ? undefined : <Icon icon="plus" />,
|
||||
onSelect: () =>
|
||||
createHttpRequest.mutate({ folderId, bodyType: BODY_TYPE_GRAPHQL, method: 'POST' }),
|
||||
},
|
||||
{
|
||||
key: 'create-grpc-request',
|
||||
label: 'gRPC Call',
|
||||
leftSlot: hideIcons ? undefined : <Icon icon="plus" />,
|
||||
onSelect: () => createGrpcRequest.mutate({ folderId }),
|
||||
},
|
||||
...((hideFolder
|
||||
? []
|
||||
: [
|
||||
{
|
||||
type: 'separator',
|
||||
},
|
||||
{
|
||||
key: 'create-folder',
|
||||
label: 'Folder',
|
||||
leftSlot: hideIcons ? undefined : <Icon icon="plus" />,
|
||||
onSelect: () => createFolder.mutate({ folderId }),
|
||||
},
|
||||
]) as DropdownItem[]),
|
||||
],
|
||||
[createFolder, createGrpcRequest, createHttpRequest, folderId, hideFolder, hideIcons],
|
||||
);
|
||||
}
|
||||
@@ -2,20 +2,35 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { trackEvent } from '../lib/analytics';
|
||||
import type { Folder } from '../lib/models';
|
||||
import { useActiveRequest } from './useActiveRequest';
|
||||
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
|
||||
import { foldersQueryKey } from './useFolders';
|
||||
import { usePrompt } from './usePrompt';
|
||||
|
||||
export function useCreateFolder() {
|
||||
const workspaceId = useActiveWorkspaceId();
|
||||
const activeRequest = useActiveRequest();
|
||||
const queryClient = useQueryClient();
|
||||
const prompt = usePrompt();
|
||||
|
||||
return useMutation<Folder, unknown, Partial<Pick<Folder, 'name' | 'sortPriority' | 'folderId'>>>({
|
||||
mutationFn: (patch) => {
|
||||
mutationFn: async (patch) => {
|
||||
if (workspaceId === null) {
|
||||
throw new Error("Cannot create folder when there's no active workspace");
|
||||
}
|
||||
patch.name = patch.name || 'New Folder';
|
||||
patch.name =
|
||||
patch.name ||
|
||||
(await prompt({
|
||||
id: 'new-folder',
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
defaultValue: 'Folder',
|
||||
title: 'New Folder',
|
||||
confirmLabel: 'Create',
|
||||
placeholder: 'Name',
|
||||
}));
|
||||
patch.sortPriority = patch.sortPriority || -Date.now();
|
||||
patch.folderId = patch.folderId || activeRequest?.folderId;
|
||||
return invoke('cmd_create_folder', { workspaceId, ...patch });
|
||||
},
|
||||
onSettled: () => trackEvent('folder', 'create'),
|
||||
|
||||
@@ -3,13 +3,14 @@ import { invoke } from '@tauri-apps/api';
|
||||
import { trackEvent } from '../lib/analytics';
|
||||
import type { GrpcRequest } from '../lib/models';
|
||||
import { useActiveEnvironmentId } from './useActiveEnvironmentId';
|
||||
import { useActiveRequest } from './useActiveRequest';
|
||||
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
|
||||
import { useAppRoutes } from './useAppRoutes';
|
||||
|
||||
export function useCreateGrpcRequest() {
|
||||
const workspaceId = useActiveWorkspaceId();
|
||||
const activeEnvironmentId = useActiveEnvironmentId();
|
||||
const activeRequest = null;
|
||||
const activeRequest = useActiveRequest();
|
||||
const routes = useAppRoutes();
|
||||
|
||||
return useMutation<
|
||||
@@ -24,13 +25,13 @@ export function useCreateGrpcRequest() {
|
||||
if (patch.sortPriority === undefined) {
|
||||
if (activeRequest != null) {
|
||||
// Place above currently-active request
|
||||
// patch.sortPriority = activeRequest.sortPriority + 0.0001;
|
||||
patch.sortPriority = activeRequest.sortPriority + 0.0001;
|
||||
} else {
|
||||
// Place at the very top
|
||||
patch.sortPriority = -Date.now();
|
||||
}
|
||||
}
|
||||
// patch.folderId = patch.folderId; // TODO: || activeRequest?.folderId;
|
||||
patch.folderId = patch.folderId || activeRequest?.folderId;
|
||||
return invoke('cmd_create_grpc_request', { workspaceId, name: '', ...patch });
|
||||
},
|
||||
onSettled: () => trackEvent('grpc_request', 'create'),
|
||||
|
||||
@@ -3,12 +3,25 @@ import { invoke } from '@tauri-apps/api';
|
||||
import { trackEvent } from '../lib/analytics';
|
||||
import type { Workspace } from '../lib/models';
|
||||
import { useAppRoutes } from './useAppRoutes';
|
||||
import { usePrompt } from './usePrompt';
|
||||
|
||||
export function useCreateWorkspace({ navigateAfter }: { navigateAfter: boolean }) {
|
||||
const routes = useAppRoutes();
|
||||
return useMutation<Workspace, unknown, Pick<Workspace, 'name'>>({
|
||||
mutationFn: (patch) => {
|
||||
return invoke('cmd_create_workspace', patch);
|
||||
const prompt = usePrompt();
|
||||
return useMutation<Workspace, unknown, Partial<Pick<Workspace, 'name'>>>({
|
||||
mutationFn: async ({ name: patchName }) => {
|
||||
const name =
|
||||
patchName ??
|
||||
(await prompt({
|
||||
id: 'new-workspace',
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
defaultValue: 'My Workspace',
|
||||
title: 'New Workspace',
|
||||
confirmLabel: 'Create',
|
||||
placeholder: 'My Workspace',
|
||||
}));
|
||||
return invoke('cmd_create_workspace', { name });
|
||||
},
|
||||
onSettled: () => trackEvent('workspace', 'create'),
|
||||
onSuccess: async (workspace) => {
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import type { HttpRequest } from '../lib/models';
|
||||
import type { GrpcRequest, HttpRequest } from '../lib/models';
|
||||
import { useDeleteAnyGrpcRequest } from './useDeleteAnyGrpcRequest';
|
||||
import { useDeleteAnyHttpRequest } from './useDeleteAnyHttpRequest';
|
||||
|
||||
export function useDeleteRequest(id: string | null) {
|
||||
const deleteAnyRequest = useDeleteAnyHttpRequest();
|
||||
export function useDeleteRequest(request: HttpRequest | GrpcRequest | null) {
|
||||
const deleteAnyHttpRequest = useDeleteAnyHttpRequest();
|
||||
const deleteAnyGrpcRequest = useDeleteAnyGrpcRequest();
|
||||
|
||||
return useMutation<HttpRequest | null, string>({
|
||||
mutationFn: () => deleteAnyRequest.mutateAsync(id ?? 'n/a'),
|
||||
return useMutation<void, string>({
|
||||
mutationFn: async () => {
|
||||
if (request?.model === 'http_request') {
|
||||
await deleteAnyHttpRequest.mutateAsync(request.id);
|
||||
} else if (request?.model === 'grpc_request') {
|
||||
await deleteAnyGrpcRequest.mutateAsync(request.id);
|
||||
} else {
|
||||
// Request is null
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { trackEvent } from '../lib/analytics';
|
||||
import { setKeyValue } from '../lib/keyValueStore';
|
||||
import type { GrpcRequest } from '../lib/models';
|
||||
import { useActiveEnvironmentId } from './useActiveEnvironmentId';
|
||||
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
|
||||
import { useAppRoutes } from './useAppRoutes';
|
||||
import { protoFilesArgs, useGrpcProtoFiles } from './useGrpcProtoFiles';
|
||||
|
||||
export function useDuplicateGrpcRequest({
|
||||
id,
|
||||
@@ -16,6 +18,7 @@ export function useDuplicateGrpcRequest({
|
||||
const activeWorkspaceId = useActiveWorkspaceId();
|
||||
const activeEnvironmentId = useActiveEnvironmentId();
|
||||
const routes = useAppRoutes();
|
||||
const protoFiles = useGrpcProtoFiles(id);
|
||||
return useMutation<GrpcRequest, string>({
|
||||
mutationFn: async () => {
|
||||
if (id === null) throw new Error("Can't duplicate a null grpc request");
|
||||
@@ -23,6 +26,9 @@ export function useDuplicateGrpcRequest({
|
||||
},
|
||||
onSettled: () => trackEvent('grpc_request', 'duplicate'),
|
||||
onSuccess: async (request) => {
|
||||
// Also copy proto files to new request
|
||||
await setKeyValue({ ...protoFilesArgs(request.id), value: protoFiles.value ?? [] });
|
||||
|
||||
if (navigateAfter && activeWorkspaceId !== null) {
|
||||
routes.navigate('request', {
|
||||
workspaceId: activeWorkspaceId,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { emit } from '@tauri-apps/api/event';
|
||||
import { trackEvent } from '../lib/analytics';
|
||||
import { minPromiseMillis } from '../lib/minPromiseMillis';
|
||||
import type { GrpcConnection, GrpcRequest } from '../lib/models';
|
||||
import { useActiveEnvironmentId } from './useActiveEnvironmentId';
|
||||
@@ -21,24 +22,28 @@ export function useGrpc(
|
||||
|
||||
const go = useMutation<void, string>({
|
||||
mutationFn: async () => await invoke('cmd_grpc_go', { requestId, environmentId, protoFiles }),
|
||||
onSettled: () => trackEvent('grpc_request', 'send'),
|
||||
});
|
||||
|
||||
const send = useMutation({
|
||||
mutationFn: async ({ message }: { message: string }) =>
|
||||
await emit(`grpc_client_msg_${conn?.id ?? 'none'}`, { Message: message }),
|
||||
onSettled: () => trackEvent('grpc_connection', 'send'),
|
||||
});
|
||||
|
||||
const cancel = useMutation({
|
||||
mutationKey: ['grpc_cancel', conn?.id ?? 'n/a'],
|
||||
mutationFn: async () => await emit(`grpc_client_msg_${conn?.id ?? 'none'}`, 'Cancel'),
|
||||
onSettled: () => trackEvent('grpc_connection', 'cancel'),
|
||||
});
|
||||
|
||||
const commit = useMutation({
|
||||
mutationKey: ['grpc_commit', conn?.id ?? 'n/a'],
|
||||
mutationFn: async () => await emit(`grpc_client_msg_${conn?.id ?? 'none'}`, 'Commit'),
|
||||
onSettled: () => trackEvent('grpc_connection', 'commit'),
|
||||
});
|
||||
|
||||
const debouncedUrl = useDebouncedValue<string>(req?.url ?? 'n/a', 1000);
|
||||
const debouncedUrl = useDebouncedValue<string>(req?.url ?? 'n/a', 500);
|
||||
const reflect = useQuery<ReflectResponseService[], string>({
|
||||
enabled: req != null,
|
||||
queryKey: ['grpc_reflect', req?.id ?? 'n/a', debouncedUrl],
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { NAMESPACE_GLOBAL } from '../lib/keyValueStore';
|
||||
import { useKeyValue } from './useKeyValue';
|
||||
|
||||
export function useGrpcProtoFiles(activeRequestId: string | null) {
|
||||
return useKeyValue<string[]>({
|
||||
export function protoFilesArgs(requestId: string | null) {
|
||||
return {
|
||||
namespace: NAMESPACE_GLOBAL,
|
||||
key: ['proto_files', activeRequestId ?? 'n/a'],
|
||||
defaultValue: [],
|
||||
});
|
||||
key: ['proto_files', requestId ?? 'n/a'],
|
||||
};
|
||||
}
|
||||
|
||||
export function useGrpcProtoFiles(activeRequestId: string | null) {
|
||||
return useKeyValue<string[]>({ ...protoFilesArgs(activeRequestId), fallback: [] });
|
||||
}
|
||||
|
||||
@@ -4,8 +4,9 @@ import { capitalize } from '../lib/capitalize';
|
||||
import { debounce } from '../lib/debounce';
|
||||
import { useOsInfo } from './useOsInfo';
|
||||
|
||||
const HOLD_KEYS = ['Shift', 'Control', 'Command', 'Alt', 'Meta'];
|
||||
|
||||
export type HotkeyAction =
|
||||
| 'popup.close'
|
||||
| 'environmentEditor.toggle'
|
||||
| 'hotkeys.showHelp'
|
||||
| 'grpc_request.send'
|
||||
@@ -20,7 +21,6 @@ export type HotkeyAction =
|
||||
| 'urlBar.focus';
|
||||
|
||||
const hotkeys: Record<HotkeyAction, string[]> = {
|
||||
'popup.close': ['Escape'],
|
||||
'environmentEditor.toggle': ['CmdCtrl+Shift+e'],
|
||||
'grpc_request.send': ['CmdCtrl+Enter', 'CmdCtrl+r'],
|
||||
'hotkeys.showHelp': ['CmdCtrl+Shift+/'],
|
||||
@@ -36,7 +36,6 @@ const hotkeys: Record<HotkeyAction, string[]> = {
|
||||
};
|
||||
|
||||
const hotkeyLabels: Record<HotkeyAction, string> = {
|
||||
'popup.close': 'Close Dropdown',
|
||||
'environmentEditor.toggle': 'Edit Environments',
|
||||
'grpc_request.send': 'Send Message',
|
||||
'hotkeys.showHelp': 'Show Keyboard Shortcuts',
|
||||
@@ -61,17 +60,6 @@ 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);
|
||||
@@ -83,7 +71,7 @@ export function useAnyHotkey(
|
||||
}, [callback]);
|
||||
|
||||
useEffect(() => {
|
||||
// Sometimes the keyup event doesn't fire, so we clear the keys after a timeout
|
||||
// Sometimes the keyup event doesn't fire (eg, cmd+Tab), so we clear the keys after a timeout
|
||||
const clearCurrentKeys = debounce(() => currentKeys.current.clear(), 5000);
|
||||
|
||||
const down = (e: KeyboardEvent) => {
|
||||
@@ -91,29 +79,55 @@ export function useAnyHotkey(
|
||||
return;
|
||||
}
|
||||
|
||||
currentKeys.current.add(normalizeKey(e.key, os));
|
||||
const key = normalizeKey(e.key, os);
|
||||
|
||||
// Don't add hold keys
|
||||
if (HOLD_KEYS.includes(key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
currentKeys.current.add(key);
|
||||
|
||||
const currentKeysWithModifiers = new Set(currentKeys.current);
|
||||
if (e.altKey) currentKeysWithModifiers.add(normalizeKey('Alt', os));
|
||||
if (e.ctrlKey) currentKeysWithModifiers.add(normalizeKey('Control', os));
|
||||
if (e.metaKey) currentKeysWithModifiers.add(normalizeKey('Meta', os));
|
||||
if (e.shiftKey) currentKeysWithModifiers.add(normalizeKey('Shift', os));
|
||||
|
||||
for (const [hkAction, hkKeys] of Object.entries(hotkeys) as [HotkeyAction, string[]][]) {
|
||||
for (const hkKey of hkKeys) {
|
||||
if (hkAction !== action) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const keys = hkKey.split('+');
|
||||
if (
|
||||
keys.length === currentKeys.current.size &&
|
||||
keys.every((key) => currentKeys.current.has(key))
|
||||
keys.length === currentKeysWithModifiers.size &&
|
||||
keys.every((key) => currentKeysWithModifiers.has(key))
|
||||
) {
|
||||
// Triggered hotkey!
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
callbackRef.current(hkAction, e);
|
||||
callbackRef.current(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clearCurrentKeys();
|
||||
};
|
||||
const up = (e: KeyboardEvent) => {
|
||||
if (options.enable === false) {
|
||||
return;
|
||||
}
|
||||
currentKeys.current.delete(normalizeKey(e.key, os));
|
||||
const key = normalizeKey(e.key, os);
|
||||
currentKeys.current.delete(key);
|
||||
|
||||
// Clear all keys if no longer holding modifier
|
||||
// HACK: This is to get around the case of DOWN SHIFT -> DOWN : -> UP SHIFT -> UP ;
|
||||
// As you see, the ":" is not removed because it turned into ";" when shift was released
|
||||
const isHoldingModifier = e.altKey || e.ctrlKey || e.metaKey || e.shiftKey;
|
||||
if (!isHoldingModifier) {
|
||||
currentKeys.current.clear();
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', down, { capture: true });
|
||||
document.addEventListener('keyup', up, { capture: true });
|
||||
|
||||
@@ -22,7 +22,7 @@ export function useIntrospectGraphQL(baseRequest: HttpRequest) {
|
||||
const [refetchKey, setRefetchKey] = useState<number>(0);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [error, setError] = useState<string>();
|
||||
const [introspection, setIntrospection] = useLocalStorage<IntrospectionQuery>(
|
||||
const [introspection, setIntrospection] = useLocalStorage<IntrospectionQuery | null>(
|
||||
`introspection:${baseRequest.id}`,
|
||||
);
|
||||
|
||||
@@ -61,7 +61,10 @@ export function useIntrospectGraphQL(baseRequest: HttpRequest) {
|
||||
|
||||
const runIntrospection = () => {
|
||||
fetchIntrospection()
|
||||
.catch((e) => setError(e.message))
|
||||
.catch((e) => {
|
||||
setIntrospection(null);
|
||||
setError(e.message);
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
};
|
||||
|
||||
|
||||
@@ -18,16 +18,16 @@ export function keyValueQueryKey({
|
||||
export function useKeyValue<T extends Object | null>({
|
||||
namespace = DEFAULT_NAMESPACE,
|
||||
key,
|
||||
defaultValue,
|
||||
fallback,
|
||||
}: {
|
||||
namespace?: string;
|
||||
key: string | string[];
|
||||
defaultValue: T;
|
||||
fallback: T;
|
||||
}) {
|
||||
const queryClient = useQueryClient();
|
||||
const query = useQuery<T>({
|
||||
queryKey: keyValueQueryKey({ namespace, key }),
|
||||
queryFn: async () => getKeyValue({ namespace, key, fallback: defaultValue }),
|
||||
queryFn: async () => getKeyValue({ namespace, key, fallback }),
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
|
||||
@@ -40,7 +40,7 @@ export function useKeyValue<T extends Object | null>({
|
||||
const set = useCallback(
|
||||
async (value: ((v: T) => T) | T) => {
|
||||
if (typeof value === 'function') {
|
||||
await getKeyValue({ namespace, key, fallback: defaultValue }).then((kv) => {
|
||||
await getKeyValue({ namespace, key, fallback }).then((kv) => {
|
||||
const newV = value(kv);
|
||||
if (newV === kv) return;
|
||||
return mutate.mutateAsync(newV);
|
||||
@@ -51,10 +51,10 @@ export function useKeyValue<T extends Object | null>({
|
||||
await mutate.mutateAsync(value);
|
||||
}
|
||||
},
|
||||
[defaultValue, key, mutate, namespace],
|
||||
[fallback, key, mutate, namespace],
|
||||
);
|
||||
|
||||
const reset = useCallback(async () => mutate.mutateAsync(defaultValue), [mutate, defaultValue]);
|
||||
const reset = useCallback(async () => mutate.mutateAsync(fallback), [mutate, fallback]);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useKeyValue } from './useKeyValue';
|
||||
|
||||
const kvKey = (workspaceId: string) => 'recent_environments::' + workspaceId;
|
||||
const namespace = NAMESPACE_GLOBAL;
|
||||
const defaultValue: string[] = [];
|
||||
const fallback: string[] = [];
|
||||
|
||||
export function useRecentEnvironments() {
|
||||
const environments = useEnvironments();
|
||||
@@ -16,7 +16,7 @@ export function useRecentEnvironments() {
|
||||
const kv = useKeyValue<string[]>({
|
||||
key: kvKey(activeWorkspaceId ?? 'n/a'),
|
||||
namespace,
|
||||
defaultValue,
|
||||
fallback,
|
||||
});
|
||||
|
||||
// Set history when active request changes
|
||||
@@ -41,6 +41,6 @@ export async function getRecentEnvironments(workspaceId: string) {
|
||||
return getKeyValue<string[]>({
|
||||
namespace,
|
||||
key: kvKey(workspaceId),
|
||||
fallback: defaultValue,
|
||||
fallback,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useKeyValue } from './useKeyValue';
|
||||
|
||||
const kvKey = (workspaceId: string) => 'recent_requests::' + workspaceId;
|
||||
const namespace = NAMESPACE_GLOBAL;
|
||||
const defaultValue: string[] = [];
|
||||
const fallback: string[] = [];
|
||||
|
||||
export function useRecentRequests() {
|
||||
const httpRequests = useHttpRequests();
|
||||
@@ -20,7 +20,7 @@ export function useRecentRequests() {
|
||||
const kv = useKeyValue<string[]>({
|
||||
key: kvKey(activeWorkspaceId ?? 'n/a'),
|
||||
namespace,
|
||||
defaultValue,
|
||||
fallback,
|
||||
});
|
||||
|
||||
// Set history when active request changes
|
||||
@@ -45,6 +45,6 @@ export async function getRecentRequests(workspaceId: string) {
|
||||
return getKeyValue<string[]>({
|
||||
namespace,
|
||||
key: kvKey(workspaceId),
|
||||
fallback: defaultValue,
|
||||
fallback,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useWorkspaces } from './useWorkspaces';
|
||||
|
||||
const kvKey = () => 'recent_workspaces';
|
||||
const namespace = NAMESPACE_GLOBAL;
|
||||
const defaultValue: string[] = [];
|
||||
const fallback: string[] = [];
|
||||
|
||||
export function useRecentWorkspaces() {
|
||||
const workspaces = useWorkspaces();
|
||||
@@ -14,7 +14,7 @@ export function useRecentWorkspaces() {
|
||||
const kv = useKeyValue<string[]>({
|
||||
key: kvKey(),
|
||||
namespace,
|
||||
defaultValue,
|
||||
fallback,
|
||||
});
|
||||
|
||||
// Set history when active request changes
|
||||
@@ -39,6 +39,6 @@ export async function getRecentWorkspaces() {
|
||||
return getKeyValue<string[]>({
|
||||
namespace,
|
||||
key: kvKey(),
|
||||
fallback: defaultValue,
|
||||
fallback: fallback,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6,11 +6,11 @@ import { trackEvent } from '../lib/analytics';
|
||||
import type { HttpResponse } from '../lib/models';
|
||||
import { getHttpRequest } from '../lib/store';
|
||||
import { useActiveCookieJar } from './useActiveCookieJar';
|
||||
import { useActiveEnvironmentId } from './useActiveEnvironmentId';
|
||||
import { useActiveEnvironment } from './useActiveEnvironment';
|
||||
import { useAlert } from './useAlert';
|
||||
|
||||
export function useSendAnyRequest(options: { download?: boolean } = {}) {
|
||||
const environmentId = useActiveEnvironmentId();
|
||||
const environment = useActiveEnvironment();
|
||||
const alert = useAlert();
|
||||
const { activeCookieJar } = useActiveCookieJar();
|
||||
return useMutation<HttpResponse | null, string, string | null>({
|
||||
@@ -33,7 +33,7 @@ export function useSendAnyRequest(options: { download?: boolean } = {}) {
|
||||
|
||||
return invoke('cmd_send_http_request', {
|
||||
requestId: id,
|
||||
environmentId,
|
||||
environmentId: environment?.id,
|
||||
downloadDir: downloadDir,
|
||||
cookieJarId: activeCookieJar?.id,
|
||||
});
|
||||
|
||||
@@ -8,7 +8,7 @@ export function useSidebarHidden() {
|
||||
const { set, value } = useKeyValue<boolean>({
|
||||
namespace: NAMESPACE_NO_SYNC,
|
||||
key: ['sidebar_hidden', activeWorkspaceId ?? 'n/a'],
|
||||
defaultValue: false,
|
||||
fallback: false,
|
||||
});
|
||||
|
||||
return useMemo(() => {
|
||||
|
||||
@@ -18,6 +18,7 @@ export function trackEvent(
|
||||
| 'workspace',
|
||||
action:
|
||||
| 'cancel'
|
||||
| 'commit'
|
||||
| 'create'
|
||||
| 'delete'
|
||||
| 'delete_many'
|
||||
|
||||
@@ -20,9 +20,10 @@ if (osType !== 'Darwin') {
|
||||
const settings = await getSettings();
|
||||
setAppearanceOnDocument(settings.appearance as Appearance);
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
// Don't go back in history on backspace
|
||||
if (e.key === 'Backspace') e.preventDefault();
|
||||
window.addEventListener('keydown', (e) => {
|
||||
// Hack to not go back in history on backspace. Check for document body
|
||||
// or else it will prevent backspace in input fields.
|
||||
if (e.key === 'Backspace' && e.target === document.body) e.preventDefault();
|
||||
});
|
||||
|
||||
createRoot(document.getElementById('root') as HTMLElement).render(
|
||||
|
||||
Reference in New Issue
Block a user