mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-29 21:51:59 +02:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e8ec36474 | ||
|
|
52d1602d35 | ||
|
|
e5731ceb1f | ||
|
|
3ed5a47a83 | ||
|
|
262a29ca5d | ||
|
|
4a3e599128 | ||
|
|
7ebe844643 | ||
|
|
a49b72eebc | ||
|
|
bba3afa0b7 | ||
|
|
221e768b33 | ||
|
|
c2dc7e0f4a | ||
|
|
9e065c34ee | ||
|
|
2f91d541c5 | ||
|
|
948fd487ab | ||
|
|
ed6a5386a2 | ||
|
|
8a24c48fd3 | ||
|
|
d726a6f5bf | ||
|
|
8d2a2a8532 | ||
|
|
b838a6ffc1 |
15
package-lock.json
generated
15
package-lock.json
generated
@@ -34,6 +34,7 @@
|
|||||||
"format-graphql": "^1.4.0",
|
"format-graphql": "^1.4.0",
|
||||||
"framer-motion": "^9.0.4",
|
"framer-motion": "^9.0.4",
|
||||||
"lucide-react": "^0.309.0",
|
"lucide-react": "^0.309.0",
|
||||||
|
"mime": "^4.0.1",
|
||||||
"papaparse": "^5.4.1",
|
"papaparse": "^5.4.1",
|
||||||
"parse-color": "^1.0.0",
|
"parse-color": "^1.0.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
@@ -7208,6 +7209,20 @@
|
|||||||
"node": ">=8.6"
|
"node": ">=8.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mime": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime/-/mime-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-5lZ5tyrIfliMXzFtkYyekWbtRXObT9OWa8IwQ5uxTBDHucNNwniRqo0yInflj+iYi5CBa6qxadGzGarDfuEOxA==",
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/sponsors/broofa"
|
||||||
|
],
|
||||||
|
"bin": {
|
||||||
|
"mime": "bin/cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/mimic-fn": {
|
"node_modules/mimic-fn": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
|
||||||
|
|||||||
@@ -51,6 +51,7 @@
|
|||||||
"format-graphql": "^1.4.0",
|
"format-graphql": "^1.4.0",
|
||||||
"framer-motion": "^9.0.4",
|
"framer-motion": "^9.0.4",
|
||||||
"lucide-react": "^0.309.0",
|
"lucide-react": "^0.309.0",
|
||||||
|
"mime": "^4.0.1",
|
||||||
"papaparse": "^5.4.1",
|
"papaparse": "^5.4.1",
|
||||||
"parse-color": "^1.0.0",
|
"parse-color": "^1.0.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
|
|
||||||
use log::{debug, warn};
|
use log::{warn};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use sqlx::types::JsonValue;
|
use sqlx::types::JsonValue;
|
||||||
@@ -182,7 +182,7 @@ pub async fn track_event(
|
|||||||
|
|
||||||
// Disable analytics actual sending in dev
|
// Disable analytics actual sending in dev
|
||||||
if is_dev() {
|
if is_dev() {
|
||||||
debug!("track: {} {} {:?}", event, attributes_json, params);
|
// debug!("track: {} {} {:?}", event, attributes_json, params);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,15 +7,15 @@ use std::sync::Arc;
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
use http::{HeaderMap, HeaderName, HeaderValue, Method};
|
|
||||||
use http::header::{ACCEPT, USER_AGENT};
|
use http::header::{ACCEPT, USER_AGENT};
|
||||||
|
use http::{HeaderMap, HeaderName, HeaderValue, Method};
|
||||||
use log::{error, info, warn};
|
use log::{error, info, warn};
|
||||||
use reqwest::{multipart, Url};
|
|
||||||
use reqwest::redirect::Policy;
|
use reqwest::redirect::Policy;
|
||||||
|
use reqwest::{multipart, Url};
|
||||||
use sqlx::types::{Json, JsonValue};
|
use sqlx::types::{Json, JsonValue};
|
||||||
use tauri::{Manager, Window};
|
use tauri::{Manager, Window};
|
||||||
use tokio::sync::oneshot;
|
use tokio::sync::oneshot;
|
||||||
use tokio::sync::watch::{Receiver};
|
use tokio::sync::watch::Receiver;
|
||||||
|
|
||||||
use crate::{models, render, response_err};
|
use crate::{models, render, response_err};
|
||||||
|
|
||||||
@@ -244,6 +244,21 @@ pub async fn send_http_request(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
request_builder = request_builder.form(&form_params);
|
request_builder = request_builder.form(&form_params);
|
||||||
|
} else if body_type == "binary" && request_body.contains_key("filePath") {
|
||||||
|
let file_path = request_body
|
||||||
|
.get("filePath")
|
||||||
|
.ok_or("filePath not set")?
|
||||||
|
.as_str()
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
match fs::read(file_path).map_err(|e| e.to_string()) {
|
||||||
|
Ok(f) => {
|
||||||
|
request_builder = request_builder.body(f);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
return response_err(response, e, window).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if body_type == "multipart/form-data" && request_body.contains_key("form") {
|
} else if body_type == "multipart/form-data" && request_body.contains_key("form") {
|
||||||
let mut multipart_form = multipart::Form::new();
|
let mut multipart_form = multipart::Form::new();
|
||||||
if let Some(form_definition) = request_body.get("form") {
|
if let Some(form_definition) = request_body.get("form") {
|
||||||
@@ -253,12 +268,13 @@ pub async fn send_http_request(
|
|||||||
.unwrap_or(empty_bool)
|
.unwrap_or(empty_bool)
|
||||||
.as_bool()
|
.as_bool()
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
let name = p
|
let name_raw = p
|
||||||
.get("name")
|
.get("name")
|
||||||
.unwrap_or(empty_string)
|
.unwrap_or(empty_string)
|
||||||
.as_str()
|
.as_str()
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
if !enabled || name.is_empty() {
|
|
||||||
|
if !enabled || name_raw.is_empty() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -267,24 +283,40 @@ pub async fn send_http_request(
|
|||||||
.unwrap_or(empty_string)
|
.unwrap_or(empty_string)
|
||||||
.as_str()
|
.as_str()
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let value = p
|
let value_raw = p
|
||||||
.get("value")
|
.get("value")
|
||||||
.unwrap_or(empty_string)
|
.unwrap_or(empty_string)
|
||||||
.as_str()
|
.as_str()
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
multipart_form = multipart_form.part(
|
|
||||||
render::render(name, &workspace, environment_ref),
|
let name = render::render(name_raw, &workspace, environment_ref);
|
||||||
match !file.is_empty() {
|
let part = if file.is_empty() {
|
||||||
true => {
|
multipart::Part::text(render::render(
|
||||||
multipart::Part::bytes(fs::read(file).map_err(|e| e.to_string())?)
|
value_raw,
|
||||||
|
&workspace,
|
||||||
|
environment_ref,
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
match fs::read(file) {
|
||||||
|
Ok(f) => multipart::Part::bytes(f),
|
||||||
|
Err(e) => {
|
||||||
|
return response_err(response, e.to_string(), window).await;
|
||||||
}
|
}
|
||||||
false => multipart::Part::text(render::render(
|
}
|
||||||
value,
|
};
|
||||||
&workspace,
|
|
||||||
environment_ref,
|
let ct_raw = p
|
||||||
)),
|
.get("contentType")
|
||||||
},
|
.unwrap_or(empty_string)
|
||||||
);
|
.as_str()
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
multipart_form = multipart_form.part(name, if ct_raw.is_empty() {
|
||||||
|
part
|
||||||
|
} else {
|
||||||
|
let ct = render::render(ct_raw, &workspace, environment_ref);
|
||||||
|
part.mime_str(ct.as_str()).map_err(|e| e.to_string())?
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
headers.remove("Content-Type"); // reqwest will add this automatically
|
headers.remove("Content-Type"); // reqwest will add this automatically
|
||||||
@@ -307,11 +339,11 @@ pub async fn send_http_request(
|
|||||||
let start = std::time::Instant::now();
|
let start = std::time::Instant::now();
|
||||||
|
|
||||||
let (resp_tx, resp_rx) = oneshot::channel();
|
let (resp_tx, resp_rx) = oneshot::channel();
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let _ = resp_tx.send(client.execute(sendable_req).await);
|
let _ = resp_tx.send(client.execute(sendable_req).await);
|
||||||
});
|
});
|
||||||
|
|
||||||
let raw_response = tokio::select! {
|
let raw_response = tokio::select! {
|
||||||
Ok(r) = resp_rx => {r}
|
Ok(r) = resp_rx => {r}
|
||||||
_ = cancel_rx.changed() => {
|
_ = cancel_rx.changed() => {
|
||||||
|
|||||||
@@ -42,21 +42,7 @@ use window_ext::TrafficLightWindowExt;
|
|||||||
use crate::analytics::{AnalyticsAction, AnalyticsResource};
|
use crate::analytics::{AnalyticsAction, AnalyticsResource};
|
||||||
use crate::grpc::metadata_to_map;
|
use crate::grpc::metadata_to_map;
|
||||||
use crate::http::send_http_request;
|
use crate::http::send_http_request;
|
||||||
use crate::models::{
|
use crate::models::{cancel_pending_grpc_connections, cancel_pending_responses, CookieJar, create_http_response, delete_all_grpc_connections, delete_all_http_responses, delete_cookie_jar, delete_environment, delete_folder, delete_grpc_connection, delete_grpc_request, delete_http_request, delete_http_response, delete_workspace, duplicate_grpc_request, duplicate_http_request, Environment, EnvironmentVariable, Folder, get_cookie_jar, get_environment, get_folder, get_grpc_connection, get_grpc_request, get_http_request, get_http_response, get_key_value_raw, get_or_create_settings, get_workspace, get_workspace_export_resources, GrpcConnection, GrpcEvent, GrpcEventType, GrpcRequest, HttpRequest, HttpRequestHeader, HttpResponse, KeyValue, list_cookie_jars, list_environments, list_folders, list_grpc_connections, list_grpc_events, list_grpc_requests, list_requests, list_responses, list_workspaces, set_key_value_raw, Settings, update_response_if_id, update_settings, upsert_cookie_jar, upsert_environment, upsert_folder, upsert_grpc_connection, upsert_grpc_event, upsert_grpc_request, upsert_http_request, upsert_workspace, Workspace, WorkspaceExportResources};
|
||||||
cancel_pending_grpc_connections, cancel_pending_responses, CookieJar,
|
|
||||||
create_http_response, delete_all_grpc_connections, delete_all_http_responses, delete_cookie_jar,
|
|
||||||
delete_environment, delete_folder, delete_grpc_connection, delete_grpc_request,
|
|
||||||
delete_http_request, delete_http_response, delete_workspace, duplicate_grpc_request,
|
|
||||||
duplicate_http_request, Environment, EnvironmentVariable, Folder, get_cookie_jar,
|
|
||||||
get_environment, get_folder, get_grpc_connection, get_grpc_request, get_http_request,
|
|
||||||
get_http_response, get_key_value_raw, get_or_create_settings, get_workspace,
|
|
||||||
get_workspace_export_resources, GrpcConnection, GrpcEvent, GrpcEventType, GrpcRequest,
|
|
||||||
HttpRequest, HttpResponse, KeyValue, list_cookie_jars, list_environments,
|
|
||||||
list_folders, list_grpc_connections, list_grpc_events, list_grpc_requests,
|
|
||||||
list_requests, list_responses, list_workspaces, set_key_value_raw, Settings,
|
|
||||||
update_response_if_id, update_settings, upsert_cookie_jar, upsert_environment, upsert_folder, upsert_grpc_connection,
|
|
||||||
upsert_grpc_event, upsert_grpc_request, upsert_http_request, upsert_workspace, Workspace, WorkspaceExportResources,
|
|
||||||
};
|
|
||||||
use crate::plugin::ImportResult;
|
use crate::plugin::ImportResult;
|
||||||
use crate::updates::{update_mode_from_str, UpdateMode, YaakUpdater};
|
use crate::updates::{update_mode_from_str, UpdateMode, YaakUpdater};
|
||||||
|
|
||||||
@@ -1054,6 +1040,7 @@ async fn cmd_create_http_request(
|
|||||||
sort_priority: f64,
|
sort_priority: f64,
|
||||||
folder_id: Option<&str>,
|
folder_id: Option<&str>,
|
||||||
method: Option<&str>,
|
method: Option<&str>,
|
||||||
|
headers: Option<Vec<HttpRequestHeader>>,
|
||||||
body_type: Option<&str>,
|
body_type: Option<&str>,
|
||||||
w: Window,
|
w: Window,
|
||||||
) -> Result<HttpRequest, String> {
|
) -> Result<HttpRequest, String> {
|
||||||
@@ -1065,6 +1052,7 @@ async fn cmd_create_http_request(
|
|||||||
folder_id: folder_id.map(|s| s.to_string()),
|
folder_id: folder_id.map(|s| s.to_string()),
|
||||||
body_type: body_type.map(|s| s.to_string()),
|
body_type: body_type.map(|s| s.to_string()),
|
||||||
method: method.map(|s| s.to_string()).unwrap_or("GET".to_string()),
|
method: method.map(|s| s.to_string()).unwrap_or("GET".to_string()),
|
||||||
|
headers: Json(headers.unwrap_or_default()),
|
||||||
sort_priority,
|
sort_priority,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
},
|
},
|
||||||
"package": {
|
"package": {
|
||||||
"productName": "Yaak",
|
"productName": "Yaak",
|
||||||
"version": "2024.3.2"
|
"version": "2024.3.6"
|
||||||
},
|
},
|
||||||
"tauri": {
|
"tauri": {
|
||||||
"windows": [],
|
"windows": [],
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
"scope": [
|
"scope": [
|
||||||
"$RESOURCE/*",
|
"$RESOURCE/*",
|
||||||
"$APPDATA/responses/*"
|
"$APPDATA/responses/*"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"shell": {
|
"shell": {
|
||||||
"all": false,
|
"all": false,
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { createBrowserRouter, Navigate, Outlet, RouterProvider, useParams } from 'react-router-dom';
|
import { createBrowserRouter, Navigate, RouterProvider, useParams } from 'react-router-dom';
|
||||||
import { routePaths, useAppRoutes } from '../hooks/useAppRoutes';
|
import { routePaths, useAppRoutes } from '../hooks/useAppRoutes';
|
||||||
import { DialogProvider } from './DialogContext';
|
import { DefaultLayout } from './DefaultLayout';
|
||||||
import { GlobalHooks } from './GlobalHooks';
|
import { RedirectToLatestWorkspace } from './RedirectToLatestWorkspace';
|
||||||
import RouteError from './RouteError';
|
import RouteError from './RouteError';
|
||||||
import Workspace from './Workspace';
|
import Workspace from './Workspace';
|
||||||
import { RedirectToLatestWorkspace } from './RedirectToLatestWorkspace';
|
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
@@ -58,7 +57,7 @@ function RedirectLegacyEnvironmentURLs() {
|
|||||||
}>();
|
}>();
|
||||||
const environmentId = rawEnvironmentId === '__default__' ? undefined : rawEnvironmentId;
|
const environmentId = rawEnvironmentId === '__default__' ? undefined : rawEnvironmentId;
|
||||||
|
|
||||||
let to = '/';
|
let to;
|
||||||
if (workspaceId != null && requestId != null) {
|
if (workspaceId != null && requestId != null) {
|
||||||
to = routes.paths.request({ workspaceId, environmentId, requestId });
|
to = routes.paths.request({ workspaceId, environmentId, requestId });
|
||||||
} else if (workspaceId != null) {
|
} else if (workspaceId != null) {
|
||||||
@@ -69,12 +68,3 @@ function RedirectLegacyEnvironmentURLs() {
|
|||||||
|
|
||||||
return <Navigate to={to} />;
|
return <Navigate to={to} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DefaultLayout() {
|
|
||||||
return (
|
|
||||||
<DialogProvider>
|
|
||||||
<Outlet />
|
|
||||||
<GlobalHooks />
|
|
||||||
</DialogProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
82
src-web/components/BinaryFileEditor.tsx
Normal file
82
src-web/components/BinaryFileEditor.tsx
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import { open } from '@tauri-apps/api/dialog';
|
||||||
|
import mime from 'mime';
|
||||||
|
import { useKeyValue } from '../hooks/useKeyValue';
|
||||||
|
import type { HttpRequest } from '../lib/models';
|
||||||
|
import { Banner } from './core/Banner';
|
||||||
|
import { Button } from './core/Button';
|
||||||
|
import { InlineCode } from './core/InlineCode';
|
||||||
|
import { HStack, VStack } from './core/Stacks';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
requestId: string;
|
||||||
|
contentType: string | null;
|
||||||
|
body: HttpRequest['body'];
|
||||||
|
onChange: (body: HttpRequest['body']) => void;
|
||||||
|
onChangeContentType: (contentType: string | null) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function BinaryFileEditor({
|
||||||
|
contentType,
|
||||||
|
body,
|
||||||
|
onChange,
|
||||||
|
onChangeContentType,
|
||||||
|
requestId,
|
||||||
|
}: Props) {
|
||||||
|
const ignoreContentType = useKeyValue<boolean>({
|
||||||
|
namespace: 'global',
|
||||||
|
key: ['ignore_content_type', requestId],
|
||||||
|
fallback: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleClick = async () => {
|
||||||
|
await ignoreContentType.set(false);
|
||||||
|
const path = await open({
|
||||||
|
title: 'Select File',
|
||||||
|
multiple: false,
|
||||||
|
});
|
||||||
|
if (path) {
|
||||||
|
onChange({ filePath: path });
|
||||||
|
} else {
|
||||||
|
onChange({ filePath: undefined });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const filePath = typeof body.filePath === 'string' ? body.filePath : undefined;
|
||||||
|
const mimeType = mime.getType(filePath ?? '') ?? 'application/octet-stream';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VStack space={2}>
|
||||||
|
<HStack space={2} alignItems="center">
|
||||||
|
<Button variant="border" color="gray" size="sm" onClick={handleClick}>
|
||||||
|
Choose File
|
||||||
|
</Button>
|
||||||
|
<div className="text-xs font-mono truncate rtl pr-3 text-gray-800">
|
||||||
|
{/* Special character to insert ltr text in rtl element without making things wonky */}
|
||||||
|
‎
|
||||||
|
{filePath ?? 'Select File'}
|
||||||
|
</div>
|
||||||
|
</HStack>
|
||||||
|
{filePath != null && mimeType !== contentType && !ignoreContentType.value && (
|
||||||
|
<Banner className="mt-3 !py-5">
|
||||||
|
<div className="text-sm mb-4 text-center">
|
||||||
|
<div>Set Content-Type header</div>
|
||||||
|
<InlineCode>{mimeType}</InlineCode> for current request?
|
||||||
|
</div>
|
||||||
|
<HStack space={1.5} justifyContent="center">
|
||||||
|
<Button
|
||||||
|
variant="solid"
|
||||||
|
color="gray"
|
||||||
|
size="xs"
|
||||||
|
onClick={() => onChangeContentType(mimeType)}
|
||||||
|
>
|
||||||
|
Set Header
|
||||||
|
</Button>
|
||||||
|
<Button size="xs" variant="border" onClick={() => ignoreContentType.set(true)}>
|
||||||
|
Ignore
|
||||||
|
</Button>
|
||||||
|
</HStack>
|
||||||
|
</Banner>
|
||||||
|
)}
|
||||||
|
</VStack>
|
||||||
|
);
|
||||||
|
}
|
||||||
12
src-web/components/DefaultLayout.tsx
Normal file
12
src-web/components/DefaultLayout.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { Outlet } from 'react-router-dom';
|
||||||
|
import { DialogProvider } from './DialogContext';
|
||||||
|
import { GlobalHooks } from './GlobalHooks';
|
||||||
|
|
||||||
|
export function DefaultLayout() {
|
||||||
|
return (
|
||||||
|
<DialogProvider>
|
||||||
|
<Outlet />
|
||||||
|
<GlobalHooks />
|
||||||
|
</DialogProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
|
|||||||
import { useCreateEnvironment } from '../hooks/useCreateEnvironment';
|
import { useCreateEnvironment } from '../hooks/useCreateEnvironment';
|
||||||
import { useDeleteEnvironment } from '../hooks/useDeleteEnvironment';
|
import { useDeleteEnvironment } from '../hooks/useDeleteEnvironment';
|
||||||
import { useEnvironments } from '../hooks/useEnvironments';
|
import { useEnvironments } from '../hooks/useEnvironments';
|
||||||
|
import { useKeyValue } from '../hooks/useKeyValue';
|
||||||
import { usePrompt } from '../hooks/usePrompt';
|
import { usePrompt } from '../hooks/usePrompt';
|
||||||
import { useUpdateEnvironment } from '../hooks/useUpdateEnvironment';
|
import { useUpdateEnvironment } from '../hooks/useUpdateEnvironment';
|
||||||
import { useUpdateWorkspace } from '../hooks/useUpdateWorkspace';
|
import { useUpdateWorkspace } from '../hooks/useUpdateWorkspace';
|
||||||
@@ -59,14 +60,16 @@ export const EnvironmentEditDialog = function ({ initialEnvironment }: Props) {
|
|||||||
<SidebarButton
|
<SidebarButton
|
||||||
active={selectedEnvironment?.id == null}
|
active={selectedEnvironment?.id == null}
|
||||||
onClick={() => setSelectedEnvironmentId(null)}
|
onClick={() => setSelectedEnvironmentId(null)}
|
||||||
className="group"
|
|
||||||
environment={null}
|
environment={null}
|
||||||
rightSlot={
|
rightSlot={
|
||||||
<IconButton
|
<IconButton
|
||||||
size="sm"
|
size="sm"
|
||||||
|
iconSize="md"
|
||||||
|
color="custom"
|
||||||
title="Add sub environment"
|
title="Add sub environment"
|
||||||
icon="plusCircle"
|
icon="plusCircle"
|
||||||
iconClassName="text-gray-500 group-hover:text-gray-700"
|
iconClassName="text-gray-500 group-hover:text-gray-700"
|
||||||
|
className="group"
|
||||||
onClick={handleCreateEnvironment}
|
onClick={handleCreateEnvironment}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@@ -113,6 +116,11 @@ const EnvironmentEditor = function ({
|
|||||||
workspace: Workspace;
|
workspace: Workspace;
|
||||||
className?: string;
|
className?: string;
|
||||||
}) {
|
}) {
|
||||||
|
const valueVisibility = useKeyValue<boolean>({
|
||||||
|
namespace: 'global',
|
||||||
|
key: 'environmentValueVisibility',
|
||||||
|
fallback: true,
|
||||||
|
});
|
||||||
const environments = useEnvironments();
|
const environments = useEnvironments();
|
||||||
const updateEnvironment = useUpdateEnvironment(environment?.id ?? null);
|
const updateEnvironment = useUpdateEnvironment(environment?.id ?? null);
|
||||||
const updateWorkspace = useUpdateWorkspace(workspace.id);
|
const updateWorkspace = useUpdateWorkspace(workspace.id);
|
||||||
@@ -164,15 +172,26 @@ const EnvironmentEditor = function ({
|
|||||||
return (
|
return (
|
||||||
<VStack space={4} className={classNames(className, 'pl-4')}>
|
<VStack space={4} className={classNames(className, 'pl-4')}>
|
||||||
<HStack space={2} className="justify-between">
|
<HStack space={2} className="justify-between">
|
||||||
<Heading className="w-full flex items-center">
|
<Heading className="w-full flex items-center gap-1">
|
||||||
<div>{environment?.name ?? 'Global Variables'}</div>
|
<div>{environment?.name ?? 'Global Variables'}</div>
|
||||||
|
<IconButton
|
||||||
|
iconClassName="text-gray-600"
|
||||||
|
size="sm"
|
||||||
|
icon={valueVisibility.value ? 'eye' : 'eyeClosed'}
|
||||||
|
title={valueVisibility.value ? 'Hide Values' : 'Reveal Values'}
|
||||||
|
onClick={() => {
|
||||||
|
return valueVisibility.set((v) => !v);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Heading>
|
</Heading>
|
||||||
</HStack>
|
</HStack>
|
||||||
<PairEditor
|
<PairEditor
|
||||||
|
className="pr-2"
|
||||||
nameAutocomplete={nameAutocomplete}
|
nameAutocomplete={nameAutocomplete}
|
||||||
nameAutocompleteVariables={false}
|
nameAutocompleteVariables={false}
|
||||||
namePlaceholder="VAR_NAME"
|
namePlaceholder="VAR_NAME"
|
||||||
nameValidate={validateName}
|
nameValidate={validateName}
|
||||||
|
valueType={valueVisibility.value ? 'text' : 'password'}
|
||||||
valueAutocompleteVariables={false}
|
valueAutocompleteVariables={false}
|
||||||
forceUpdateKey={environment?.id ?? workspace?.id ?? 'n/a'}
|
forceUpdateKey={environment?.id ?? workspace?.id ?? 'n/a'}
|
||||||
pairs={variables}
|
pairs={variables}
|
||||||
@@ -216,8 +235,8 @@ function SidebarButton({
|
|||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
className,
|
className,
|
||||||
'w-full grid grid-cols-[minmax(0,1fr)_auto] items-center',
|
'w-full grid grid-cols-[minmax(0,1fr)_auto] items-center gap-0.5',
|
||||||
'px-1', // Padding to show focus border
|
'px-2', // Padding to show focus border
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
@@ -225,7 +244,7 @@ function SidebarButton({
|
|||||||
size="xs"
|
size="xs"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'w-full',
|
'w-full',
|
||||||
active ? 'text-gray-800' : 'text-gray-600 hover:text-gray-700',
|
active ? 'text-gray-800 bg-highlightSecondary' : 'text-gray-600 hover:text-gray-700',
|
||||||
)}
|
)}
|
||||||
justify="start"
|
justify="start"
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { PairEditor } from './core/PairEditor';
|
|||||||
type Props = {
|
type Props = {
|
||||||
forceUpdateKey: string;
|
forceUpdateKey: string;
|
||||||
body: HttpRequest['body'];
|
body: HttpRequest['body'];
|
||||||
onChange: (headers: HttpRequest['body']) => void;
|
onChange: (body: HttpRequest['body']) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function FormMultipartEditor({ body, forceUpdateKey, onChange }: Props) {
|
export function FormMultipartEditor({ body, forceUpdateKey, onChange }: Props) {
|
||||||
@@ -16,6 +16,7 @@ export function FormMultipartEditor({ body, forceUpdateKey, onChange }: Props) {
|
|||||||
enabled: p.enabled,
|
enabled: p.enabled,
|
||||||
name: p.name,
|
name: p.name,
|
||||||
value: p.file ?? p.value,
|
value: p.file ?? p.value,
|
||||||
|
contentType: p.contentType,
|
||||||
isFile: !!p.file,
|
isFile: !!p.file,
|
||||||
})),
|
})),
|
||||||
[body.form],
|
[body.form],
|
||||||
@@ -27,6 +28,7 @@ export function FormMultipartEditor({ body, forceUpdateKey, onChange }: Props) {
|
|||||||
form: pairs.map((p) => ({
|
form: pairs.map((p) => ({
|
||||||
enabled: p.enabled,
|
enabled: p.enabled,
|
||||||
name: p.name,
|
name: p.name,
|
||||||
|
contentType: p.contentType,
|
||||||
file: p.isFile ? p.value : undefined,
|
file: p.isFile ? p.value : undefined,
|
||||||
value: p.isFile ? undefined : p.value,
|
value: p.isFile ? undefined : p.value,
|
||||||
})),
|
})),
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { appWindow } from '@tauri-apps/api/window';
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
import { cookieJarsQueryKey } from '../hooks/useCookieJars';
|
import { cookieJarsQueryKey } from '../hooks/useCookieJars';
|
||||||
|
import { useGlobalCommands } from '../hooks/useGlobalCommands';
|
||||||
import { grpcConnectionsQueryKey } from '../hooks/useGrpcConnections';
|
import { grpcConnectionsQueryKey } from '../hooks/useGrpcConnections';
|
||||||
import { grpcEventsQueryKey } from '../hooks/useGrpcEvents';
|
import { grpcEventsQueryKey } from '../hooks/useGrpcEvents';
|
||||||
import { grpcRequestsQueryKey } from '../hooks/useGrpcRequests';
|
import { grpcRequestsQueryKey } from '../hooks/useGrpcRequests';
|
||||||
@@ -18,7 +19,6 @@ import { settingsQueryKey } from '../hooks/useSettings';
|
|||||||
import { useSyncAppearance } from '../hooks/useSyncAppearance';
|
import { useSyncAppearance } from '../hooks/useSyncAppearance';
|
||||||
import { useSyncWindowTitle } from '../hooks/useSyncWindowTitle';
|
import { useSyncWindowTitle } from '../hooks/useSyncWindowTitle';
|
||||||
import { workspacesQueryKey } from '../hooks/useWorkspaces';
|
import { workspacesQueryKey } from '../hooks/useWorkspaces';
|
||||||
import { NAMESPACE_NO_SYNC } from '../lib/keyValueStore';
|
|
||||||
import type { Model } from '../lib/models';
|
import type { Model } from '../lib/models';
|
||||||
import { modelsEq } from '../lib/models';
|
import { modelsEq } from '../lib/models';
|
||||||
import { setPathname } from '../lib/persistPathname';
|
import { setPathname } from '../lib/persistPathname';
|
||||||
@@ -33,8 +33,8 @@ export function GlobalHooks() {
|
|||||||
useRecentRequests();
|
useRecentRequests();
|
||||||
|
|
||||||
useSyncAppearance();
|
useSyncAppearance();
|
||||||
|
|
||||||
useSyncWindowTitle();
|
useSyncWindowTitle();
|
||||||
|
useGlobalCommands();
|
||||||
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { wasUpdatedExternally } = useRequestUpdateKey(null);
|
const { wasUpdatedExternally } = useRequestUpdateKey(null);
|
||||||
@@ -142,7 +142,7 @@ function removeById<T extends { id: string }>(model: T) {
|
|||||||
|
|
||||||
const shouldIgnoreModel = (payload: Model) => {
|
const shouldIgnoreModel = (payload: Model) => {
|
||||||
if (payload.model === 'key_value') {
|
if (payload.model === 'key_value') {
|
||||||
return payload.namespace === NAMESPACE_NO_SYNC;
|
return payload.namespace === 'no_sync';
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -195,7 +195,6 @@ export function GrpcConnectionSetupPane({
|
|||||||
shortLabel: o.label,
|
shortLabel: o.label,
|
||||||
}))}
|
}))}
|
||||||
extraItems={[
|
extraItems={[
|
||||||
{ type: 'separator' },
|
|
||||||
{
|
{
|
||||||
label: 'Refresh',
|
label: 'Refresh',
|
||||||
type: 'default',
|
type: 'default',
|
||||||
|
|||||||
@@ -3,17 +3,18 @@ import { useNavigate } from 'react-router-dom';
|
|||||||
import { useAppRoutes } from '../hooks/useAppRoutes';
|
import { useAppRoutes } from '../hooks/useAppRoutes';
|
||||||
import { getRecentEnvironments } from '../hooks/useRecentEnvironments';
|
import { getRecentEnvironments } from '../hooks/useRecentEnvironments';
|
||||||
import { getRecentRequests } from '../hooks/useRecentRequests';
|
import { getRecentRequests } from '../hooks/useRecentRequests';
|
||||||
import { getRecentWorkspaces } from '../hooks/useRecentWorkspaces';
|
import { useRecentWorkspaces } from '../hooks/useRecentWorkspaces';
|
||||||
import { useWorkspaces } from '../hooks/useWorkspaces';
|
import { useWorkspaces } from '../hooks/useWorkspaces';
|
||||||
|
|
||||||
export function RedirectToLatestWorkspace() {
|
export function RedirectToLatestWorkspace() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const routes = useAppRoutes();
|
const routes = useAppRoutes();
|
||||||
const workspaces = useWorkspaces();
|
const workspaces = useWorkspaces();
|
||||||
|
const recentWorkspaces = useRecentWorkspaces();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async function () {
|
(async function () {
|
||||||
const workspaceId = (await getRecentWorkspaces())[0] ?? workspaces[0]?.id ?? 'n/a';
|
const workspaceId = recentWorkspaces[0] ?? workspaces[0]?.id ?? 'n/a';
|
||||||
const environmentId = (await getRecentEnvironments(workspaceId))[0];
|
const environmentId = (await getRecentEnvironments(workspaceId))[0];
|
||||||
const requestId = (await getRecentRequests(workspaceId))[0];
|
const requestId = (await getRecentRequests(workspaceId))[0];
|
||||||
|
|
||||||
@@ -23,7 +24,7 @@ export function RedirectToLatestWorkspace() {
|
|||||||
navigate(routes.paths.workspace({ workspaceId, environmentId }));
|
navigate(routes.paths.workspace({ workspaceId, environmentId }));
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}, [navigate, routes.paths, workspaces, workspaces.length]);
|
}, [navigate, recentWorkspaces, routes.paths, workspaces, workspaces.length]);
|
||||||
|
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,24 +6,27 @@ import { useCancelHttpResponse } from '../hooks/useCancelHttpResponse';
|
|||||||
import { useIsResponseLoading } from '../hooks/useIsResponseLoading';
|
import { useIsResponseLoading } from '../hooks/useIsResponseLoading';
|
||||||
import { usePinnedHttpResponse } from '../hooks/usePinnedHttpResponse';
|
import { usePinnedHttpResponse } from '../hooks/usePinnedHttpResponse';
|
||||||
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
|
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
|
||||||
|
import { useContentTypeFromHeaders } from '../hooks/useContentTypeFromHeaders';
|
||||||
import { useSendRequest } from '../hooks/useSendRequest';
|
import { useSendRequest } from '../hooks/useSendRequest';
|
||||||
import { useUpdateHttpRequest } from '../hooks/useUpdateHttpRequest';
|
import { useUpdateHttpRequest } from '../hooks/useUpdateHttpRequest';
|
||||||
import { tryFormatJson } from '../lib/formatters';
|
import { tryFormatJson } from '../lib/formatters';
|
||||||
import type { HttpHeader, HttpRequest, HttpUrlParameter } from '../lib/models';
|
import type { HttpHeader, HttpRequest, HttpUrlParameter } from '../lib/models';
|
||||||
import {
|
import {
|
||||||
BODY_TYPE_OTHER,
|
|
||||||
AUTH_TYPE_BASIC,
|
AUTH_TYPE_BASIC,
|
||||||
AUTH_TYPE_BEARER,
|
AUTH_TYPE_BEARER,
|
||||||
AUTH_TYPE_NONE,
|
AUTH_TYPE_NONE,
|
||||||
|
BODY_TYPE_BINARY,
|
||||||
BODY_TYPE_FORM_MULTIPART,
|
BODY_TYPE_FORM_MULTIPART,
|
||||||
BODY_TYPE_FORM_URLENCODED,
|
BODY_TYPE_FORM_URLENCODED,
|
||||||
BODY_TYPE_GRAPHQL,
|
BODY_TYPE_GRAPHQL,
|
||||||
BODY_TYPE_JSON,
|
BODY_TYPE_JSON,
|
||||||
BODY_TYPE_NONE,
|
BODY_TYPE_NONE,
|
||||||
|
BODY_TYPE_OTHER,
|
||||||
BODY_TYPE_XML,
|
BODY_TYPE_XML,
|
||||||
} from '../lib/models';
|
} from '../lib/models';
|
||||||
import { BasicAuth } from './BasicAuth';
|
import { BasicAuth } from './BasicAuth';
|
||||||
import { BearerAuth } from './BearerAuth';
|
import { BearerAuth } from './BearerAuth';
|
||||||
|
import { BinaryFileEditor } from './BinaryFileEditor';
|
||||||
import { CountBadge } from './core/CountBadge';
|
import { CountBadge } from './core/CountBadge';
|
||||||
import { Editor } from './core/Editor';
|
import { Editor } from './core/Editor';
|
||||||
import type { TabItem } from './core/Tabs/Tabs';
|
import type { TabItem } from './core/Tabs/Tabs';
|
||||||
@@ -56,6 +59,7 @@ export const RequestPane = memo(function RequestPane({
|
|||||||
const [activeTab, setActiveTab] = useActiveTab();
|
const [activeTab, setActiveTab] = useActiveTab();
|
||||||
const [forceUpdateHeaderEditorKey, setForceUpdateHeaderEditorKey] = useState<number>(0);
|
const [forceUpdateHeaderEditorKey, setForceUpdateHeaderEditorKey] = useState<number>(0);
|
||||||
const { updateKey: forceUpdateKey } = useRequestUpdateKey(activeRequest.id ?? null);
|
const { updateKey: forceUpdateKey } = useRequestUpdateKey(activeRequest.id ?? null);
|
||||||
|
const contentType = useContentTypeFromHeaders(activeRequest.headers);
|
||||||
|
|
||||||
const tabs: TabItem[] = useMemo(
|
const tabs: TabItem[] = useMemo(
|
||||||
() => [
|
() => [
|
||||||
@@ -68,19 +72,19 @@ export const RequestPane = memo(function RequestPane({
|
|||||||
{ label: 'Url Encoded', value: BODY_TYPE_FORM_URLENCODED },
|
{ label: 'Url Encoded', value: BODY_TYPE_FORM_URLENCODED },
|
||||||
{ label: 'Multi-Part', value: BODY_TYPE_FORM_MULTIPART },
|
{ label: 'Multi-Part', value: BODY_TYPE_FORM_MULTIPART },
|
||||||
{ type: 'separator', label: 'Text Content' },
|
{ type: 'separator', label: 'Text Content' },
|
||||||
|
{ label: 'GraphQL', value: BODY_TYPE_GRAPHQL },
|
||||||
{ label: 'JSON', value: BODY_TYPE_JSON },
|
{ label: 'JSON', value: BODY_TYPE_JSON },
|
||||||
{ label: 'XML', value: BODY_TYPE_XML },
|
{ label: 'XML', value: BODY_TYPE_XML },
|
||||||
{ label: 'GraphQL', value: BODY_TYPE_GRAPHQL },
|
|
||||||
{ label: 'Other', value: BODY_TYPE_OTHER },
|
{ label: 'Other', value: BODY_TYPE_OTHER },
|
||||||
{ type: 'separator', label: 'Other' },
|
{ type: 'separator', label: 'Other' },
|
||||||
|
{ label: 'Binary File', value: BODY_TYPE_BINARY },
|
||||||
{ label: 'No Body', shortLabel: 'Body', value: BODY_TYPE_NONE },
|
{ label: 'No Body', shortLabel: 'Body', value: BODY_TYPE_NONE },
|
||||||
],
|
],
|
||||||
onChange: async (bodyType) => {
|
onChange: async (bodyType) => {
|
||||||
const patch: Partial<HttpRequest> = { bodyType };
|
const patch: Partial<HttpRequest> = { bodyType };
|
||||||
|
let newContentType: string | null | undefined;
|
||||||
if (bodyType === BODY_TYPE_NONE) {
|
if (bodyType === BODY_TYPE_NONE) {
|
||||||
patch.headers = activeRequest.headers.filter(
|
newContentType = null;
|
||||||
(h) => h.name.toLowerCase() !== 'content-type',
|
|
||||||
);
|
|
||||||
} else if (
|
} else if (
|
||||||
bodyType === BODY_TYPE_FORM_URLENCODED ||
|
bodyType === BODY_TYPE_FORM_URLENCODED ||
|
||||||
bodyType === BODY_TYPE_FORM_MULTIPART ||
|
bodyType === BODY_TYPE_FORM_MULTIPART ||
|
||||||
@@ -89,32 +93,17 @@ export const RequestPane = memo(function RequestPane({
|
|||||||
bodyType === BODY_TYPE_XML
|
bodyType === BODY_TYPE_XML
|
||||||
) {
|
) {
|
||||||
patch.method = 'POST';
|
patch.method = 'POST';
|
||||||
patch.headers = [
|
newContentType = bodyType === BODY_TYPE_OTHER ? 'text/plain' : bodyType;
|
||||||
...(activeRequest.headers.filter((h) => h.name.toLowerCase() !== 'content-type') ??
|
|
||||||
[]),
|
|
||||||
{
|
|
||||||
name: 'Content-Type',
|
|
||||||
value: bodyType,
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
} else if (bodyType == BODY_TYPE_GRAPHQL) {
|
} else if (bodyType == BODY_TYPE_GRAPHQL) {
|
||||||
patch.method = 'POST';
|
patch.method = 'POST';
|
||||||
patch.headers = [
|
newContentType = 'application/json';
|
||||||
...(activeRequest.headers.filter((h) => h.name.toLowerCase() !== 'content-type') ??
|
|
||||||
[]),
|
|
||||||
{
|
|
||||||
name: 'Content-Type',
|
|
||||||
value: 'application/json',
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Force update header editor so any changed headers are reflected
|
await updateRequest.mutateAsync(patch);
|
||||||
setTimeout(() => setForceUpdateHeaderEditorKey((u) => u + 1), 100);
|
|
||||||
|
|
||||||
updateRequest.mutate(patch);
|
if (newContentType !== undefined) {
|
||||||
|
await handleContentTypeChange(newContentType);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -171,6 +160,31 @@ export const RequestPane = memo(function RequestPane({
|
|||||||
(body: HttpRequest['body']) => updateRequest.mutate({ body }),
|
(body: HttpRequest['body']) => updateRequest.mutate({ body }),
|
||||||
[updateRequest],
|
[updateRequest],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleContentTypeChange = useCallback(
|
||||||
|
async (contentType: string | null) => {
|
||||||
|
const headers = activeRequest.headers.filter((h) => h.name.toLowerCase() !== 'content-type');
|
||||||
|
|
||||||
|
if (contentType != null) {
|
||||||
|
headers.push({
|
||||||
|
name: 'Content-Type',
|
||||||
|
value: contentType,
|
||||||
|
enabled: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await updateRequest.mutateAsync({ headers });
|
||||||
|
|
||||||
|
// Force update header editor so any changed headers are reflected
|
||||||
|
setTimeout(() => setForceUpdateHeaderEditorKey((u) => u + 1), 100);
|
||||||
|
},
|
||||||
|
[activeRequest.headers, updateRequest],
|
||||||
|
);
|
||||||
|
const handleBinaryFileChange = useCallback(
|
||||||
|
(body: HttpRequest['body']) => {
|
||||||
|
updateRequest.mutate({ body });
|
||||||
|
},
|
||||||
|
[updateRequest],
|
||||||
|
);
|
||||||
const handleBodyTextChange = useCallback(
|
const handleBodyTextChange = useCallback(
|
||||||
(text: string) => updateRequest.mutate({ body: { text } }),
|
(text: string) => updateRequest.mutate({ body: { text } }),
|
||||||
[updateRequest],
|
[updateRequest],
|
||||||
@@ -314,6 +328,14 @@ export const RequestPane = memo(function RequestPane({
|
|||||||
body={activeRequest.body}
|
body={activeRequest.body}
|
||||||
onChange={handleBodyChange}
|
onChange={handleBodyChange}
|
||||||
/>
|
/>
|
||||||
|
) : activeRequest.bodyType === BODY_TYPE_BINARY ? (
|
||||||
|
<BinaryFileEditor
|
||||||
|
requestId={activeRequest.id}
|
||||||
|
contentType={contentType}
|
||||||
|
body={activeRequest.body}
|
||||||
|
onChange={handleBinaryFileChange}
|
||||||
|
onChangeContentType={handleContentTypeChange}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<EmptyStateText>No Body</EmptyStateText>
|
<EmptyStateText>No Body</EmptyStateText>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { CSSProperties } from 'react';
|
|||||||
import { memo, useMemo } from 'react';
|
import { memo, useMemo } from 'react';
|
||||||
import { createGlobalState } from 'react-use';
|
import { createGlobalState } from 'react-use';
|
||||||
import { usePinnedHttpResponse } from '../hooks/usePinnedHttpResponse';
|
import { usePinnedHttpResponse } from '../hooks/usePinnedHttpResponse';
|
||||||
import { useResponseContentType } from '../hooks/useResponseContentType';
|
import { useContentTypeFromHeaders } from '../hooks/useContentTypeFromHeaders';
|
||||||
import { useResponseViewMode } from '../hooks/useResponseViewMode';
|
import { useResponseViewMode } from '../hooks/useResponseViewMode';
|
||||||
import type { HttpRequest } from '../lib/models';
|
import type { HttpRequest } from '../lib/models';
|
||||||
import { isResponseLoading } from '../lib/models';
|
import { isResponseLoading } from '../lib/models';
|
||||||
@@ -37,7 +37,7 @@ export const ResponsePane = memo(function ResponsePane({ style, className, activ
|
|||||||
const { activeResponse, setPinnedResponse, responses } = usePinnedHttpResponse(activeRequest);
|
const { activeResponse, setPinnedResponse, responses } = usePinnedHttpResponse(activeRequest);
|
||||||
const [viewMode, setViewMode] = useResponseViewMode(activeResponse?.requestId);
|
const [viewMode, setViewMode] = useResponseViewMode(activeResponse?.requestId);
|
||||||
const [activeTab, setActiveTab] = useActiveTab();
|
const [activeTab, setActiveTab] = useActiveTab();
|
||||||
const contentType = useResponseContentType(activeResponse);
|
const contentType = useContentTypeFromHeaders(activeResponse?.headers ?? null);
|
||||||
|
|
||||||
const tabs = useMemo<TabItem[]>(
|
const tabs = useMemo<TabItem[]>(
|
||||||
() => [
|
() => [
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ import { useUpdateAnyHttpRequest } from '../hooks/useUpdateAnyHttpRequest';
|
|||||||
import { useUpdateGrpcRequest } from '../hooks/useUpdateGrpcRequest';
|
import { useUpdateGrpcRequest } from '../hooks/useUpdateGrpcRequest';
|
||||||
import { useUpdateHttpRequest } from '../hooks/useUpdateHttpRequest';
|
import { useUpdateHttpRequest } from '../hooks/useUpdateHttpRequest';
|
||||||
import { fallbackRequestName } from '../lib/fallbackRequestName';
|
import { fallbackRequestName } from '../lib/fallbackRequestName';
|
||||||
import { NAMESPACE_NO_SYNC } from '../lib/keyValueStore';
|
|
||||||
import type { Folder, GrpcRequest, HttpRequest, Workspace } from '../lib/models';
|
import type { Folder, GrpcRequest, HttpRequest, Workspace } from '../lib/models';
|
||||||
import { isResponseLoading } from '../lib/models';
|
import { isResponseLoading } from '../lib/models';
|
||||||
import type { DropdownItem } from './core/Dropdown';
|
import type { DropdownItem } from './core/Dropdown';
|
||||||
@@ -87,7 +86,7 @@ export function Sidebar({ className }: Props) {
|
|||||||
const collapsed = useKeyValue<Record<string, boolean>>({
|
const collapsed = useKeyValue<Record<string, boolean>>({
|
||||||
key: ['sidebar_collapsed', activeWorkspace?.id ?? 'n/a'],
|
key: ['sidebar_collapsed', activeWorkspace?.id ?? 'n/a'],
|
||||||
fallback: {},
|
fallback: {},
|
||||||
namespace: NAMESPACE_NO_SYNC,
|
namespace: 'no_sync',
|
||||||
});
|
});
|
||||||
|
|
||||||
useHotKey('http_request.duplicate', async () => {
|
useHotKey('http_request.duplicate', async () => {
|
||||||
@@ -420,6 +419,19 @@ export function Sidebar({ className }: Props) {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [showMainContextMenu, setShowMainContextMenu] = useState<{
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
|
const handleMainContextMenu = useCallback((e: React.MouseEvent<HTMLElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setShowMainContextMenu({ x: e.clientX, y: e.clientY });
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const mainContextMenuItems = useCreateDropdownItems();
|
||||||
|
|
||||||
// Not ready to render yet
|
// Not ready to render yet
|
||||||
if (tree == null || collapsed.value == null) {
|
if (tree == null || collapsed.value == null) {
|
||||||
return null;
|
return null;
|
||||||
@@ -432,11 +444,17 @@ export function Sidebar({ className }: Props) {
|
|||||||
onFocus={handleFocus}
|
onFocus={handleFocus}
|
||||||
onBlur={handleBlur}
|
onBlur={handleBlur}
|
||||||
tabIndex={hidden ? -1 : 0}
|
tabIndex={hidden ? -1 : 0}
|
||||||
|
onContextMenu={handleMainContextMenu}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
className,
|
className,
|
||||||
'h-full pb-3 overflow-y-scroll overflow-x-visible hide-scrollbars pt-2',
|
'h-full pb-3 overflow-y-scroll overflow-x-visible hide-scrollbars pt-2',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
<ContextMenu
|
||||||
|
show={showMainContextMenu}
|
||||||
|
items={mainContextMenuItems}
|
||||||
|
onClose={() => setShowMainContextMenu(null)}
|
||||||
|
/>
|
||||||
<SidebarItems
|
<SidebarItems
|
||||||
treeParentMap={treeParentMap}
|
treeParentMap={treeParentMap}
|
||||||
selectedId={selectedId}
|
selectedId={selectedId}
|
||||||
|
|||||||
@@ -9,13 +9,19 @@ import type {
|
|||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useWindowSize } from 'react-use';
|
import { useWindowSize } from 'react-use';
|
||||||
import { useActiveRequest } from '../hooks/useActiveRequest';
|
import { useActiveRequest } from '../hooks/useActiveRequest';
|
||||||
|
import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
|
||||||
|
import { useActiveWorkspaceId } from '../hooks/useActiveWorkspaceId';
|
||||||
import { useImportData } from '../hooks/useImportData';
|
import { useImportData } from '../hooks/useImportData';
|
||||||
import { useIsFullscreen } from '../hooks/useIsFullscreen';
|
import { useIsFullscreen } from '../hooks/useIsFullscreen';
|
||||||
import { useOsInfo } from '../hooks/useOsInfo';
|
import { useOsInfo } from '../hooks/useOsInfo';
|
||||||
import { useSidebarHidden } from '../hooks/useSidebarHidden';
|
import { useSidebarHidden } from '../hooks/useSidebarHidden';
|
||||||
import { useSidebarWidth } from '../hooks/useSidebarWidth';
|
import { useSidebarWidth } from '../hooks/useSidebarWidth';
|
||||||
|
import { useWorkspaces } from '../hooks/useWorkspaces';
|
||||||
|
import { Banner } from './core/Banner';
|
||||||
import { Button } from './core/Button';
|
import { Button } from './core/Button';
|
||||||
import { HotKeyList } from './core/HotKeyList';
|
import { HotKeyList } from './core/HotKeyList';
|
||||||
|
import { InlineCode } from './core/InlineCode';
|
||||||
|
import { FeedbackLink } from './core/Link';
|
||||||
import { HStack } from './core/Stacks';
|
import { HStack } from './core/Stacks';
|
||||||
import { CreateDropdown } from './CreateDropdown';
|
import { CreateDropdown } from './CreateDropdown';
|
||||||
import { GrpcConnectionLayout } from './GrpcConnectionLayout';
|
import { GrpcConnectionLayout } from './GrpcConnectionLayout';
|
||||||
@@ -34,6 +40,9 @@ const drag = { gridArea: 'drag' };
|
|||||||
const WINDOW_FLOATING_SIDEBAR_WIDTH = 600;
|
const WINDOW_FLOATING_SIDEBAR_WIDTH = 600;
|
||||||
|
|
||||||
export default function Workspace() {
|
export default function Workspace() {
|
||||||
|
const workspaces = useWorkspaces();
|
||||||
|
const activeWorkspace = useActiveWorkspace();
|
||||||
|
const activeWorkspaceId = useActiveWorkspaceId();
|
||||||
const { setWidth, width, resetWidth } = useSidebarWidth();
|
const { setWidth, width, resetWidth } = useSidebarWidth();
|
||||||
const { hide, show, hidden } = useSidebarHidden();
|
const { hide, show, hidden } = useSidebarHidden();
|
||||||
const activeRequest = useActiveRequest();
|
const activeRequest = useActiveRequest();
|
||||||
@@ -119,6 +128,11 @@ export default function Workspace() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We're loading still
|
||||||
|
if (workspaces.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={styles}
|
style={styles}
|
||||||
@@ -163,7 +177,15 @@ export default function Workspace() {
|
|||||||
<HeaderSize data-tauri-drag-region style={head}>
|
<HeaderSize data-tauri-drag-region style={head}>
|
||||||
<WorkspaceHeader className="pointer-events-none" />
|
<WorkspaceHeader className="pointer-events-none" />
|
||||||
</HeaderSize>
|
</HeaderSize>
|
||||||
{activeRequest == null ? (
|
{activeWorkspace == null ? (
|
||||||
|
<div className="m-auto">
|
||||||
|
<Banner color="warning" className="max-w-[30rem]">
|
||||||
|
The active workspace{' '}
|
||||||
|
<InlineCode className="text-orange-800">{activeWorkspaceId}</InlineCode> was not found.
|
||||||
|
Select a workspace from the header menu or report this bug to <FeedbackLink />
|
||||||
|
</Banner>
|
||||||
|
</div>
|
||||||
|
) : activeRequest == null ? (
|
||||||
<HotKeyList
|
<HotKeyList
|
||||||
hotkeys={['http_request.create', 'sidebar.toggle', 'settings.show']}
|
hotkeys={['http_request.create', 'sidebar.toggle', 'settings.show']}
|
||||||
bottomSlot={
|
bottomSlot={
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import classNames from 'classnames';
|
|||||||
import { memo, useMemo } from 'react';
|
import { memo, useMemo } from 'react';
|
||||||
import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
|
import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
|
||||||
import { useAppRoutes } from '../hooks/useAppRoutes';
|
import { useAppRoutes } from '../hooks/useAppRoutes';
|
||||||
import { useCreateWorkspace } from '../hooks/useCreateWorkspace';
|
import { useCommand } from '../hooks/useCommands';
|
||||||
import { useDeleteWorkspace } from '../hooks/useDeleteWorkspace';
|
import { useDeleteWorkspace } from '../hooks/useDeleteWorkspace';
|
||||||
import { usePrompt } from '../hooks/usePrompt';
|
import { usePrompt } from '../hooks/usePrompt';
|
||||||
import { getRecentEnvironments } from '../hooks/useRecentEnvironments';
|
import { getRecentEnvironments } from '../hooks/useRecentEnvironments';
|
||||||
@@ -30,7 +30,7 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
|
|||||||
const activeWorkspaceId = activeWorkspace?.id ?? null;
|
const activeWorkspaceId = activeWorkspace?.id ?? null;
|
||||||
const updateWorkspace = useUpdateWorkspace(activeWorkspaceId);
|
const updateWorkspace = useUpdateWorkspace(activeWorkspaceId);
|
||||||
const deleteWorkspace = useDeleteWorkspace(activeWorkspace);
|
const deleteWorkspace = useDeleteWorkspace(activeWorkspace);
|
||||||
const createWorkspace = useCreateWorkspace({ navigateAfter: true });
|
const createWorkspace = useCommand('workspace.create');
|
||||||
const dialog = useDialog();
|
const dialog = useDialog();
|
||||||
const prompt = usePrompt();
|
const prompt = usePrompt();
|
||||||
const routes = useAppRoutes();
|
const routes = useAppRoutes();
|
||||||
@@ -167,10 +167,14 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
|
|||||||
<Dropdown items={items}>
|
<Dropdown items={items}>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
className={classNames(className, 'text-gray-800 !px-2 truncate')}
|
className={classNames(
|
||||||
|
className,
|
||||||
|
'text-gray-800 !px-2 truncate',
|
||||||
|
activeWorkspace === null && 'italic opacity-disabled',
|
||||||
|
)}
|
||||||
{...buttonProps}
|
{...buttonProps}
|
||||||
>
|
>
|
||||||
{activeWorkspace?.name}
|
{activeWorkspace?.name ?? 'Workspace'}
|
||||||
</Button>
|
</Button>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type { ReactNode } from 'react';
|
|||||||
interface Props {
|
interface Props {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
color?: 'danger' | 'success' | 'gray';
|
color?: 'danger' | 'warning' | 'success' | 'gray';
|
||||||
}
|
}
|
||||||
export function Banner({ children, className, color = 'gray' }: Props) {
|
export function Banner({ children, className, color = 'gray' }: Props) {
|
||||||
return (
|
return (
|
||||||
@@ -14,6 +14,7 @@ export function Banner({ children, className, color = 'gray' }: Props) {
|
|||||||
className,
|
className,
|
||||||
'border border-dashed italic px-3 py-2 rounded select-auto cursor-text',
|
'border border-dashed italic px-3 py-2 rounded select-auto cursor-text',
|
||||||
color === 'gray' && 'border-gray-500/60 bg-gray-300/10 text-gray-800',
|
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 === '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-green-500/60 bg-green-300/10 text-green-800',
|
||||||
)}
|
)}
|
||||||
|
|||||||
14
src-web/components/core/Editor/BetterMatchDecorator.ts
Normal file
14
src-web/components/core/Editor/BetterMatchDecorator.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { type DecorationSet, MatchDecorator, type ViewUpdate } from '@codemirror/view';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a custom MatchDecorator that will not decorate a match if the selection is inside it
|
||||||
|
*/
|
||||||
|
export class BetterMatchDecorator extends MatchDecorator {
|
||||||
|
updateDeco(update: ViewUpdate, deco: DecorationSet): DecorationSet {
|
||||||
|
if (!update.startState.selection.eq(update.state.selection)) {
|
||||||
|
return super.createDeco(update.view);
|
||||||
|
} else {
|
||||||
|
return super.updateDeco(update, deco);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -69,6 +69,14 @@
|
|||||||
@apply text-red-700 dark:text-red-800 bg-red-300/30 border-red-300/80 border-opacity-40 hover:border-red-300 hover:bg-red-300/40;
|
@apply text-red-700 dark:text-red-800 bg-red-300/30 border-red-300/80 border-opacity-40 hover:border-red-300 hover:bg-red-300/40;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hyperlink-widget {
|
||||||
|
& > * {
|
||||||
|
@apply underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
-webkit-text-security: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.cm-singleline {
|
&.cm-singleline {
|
||||||
@@ -103,10 +111,10 @@
|
|||||||
@apply font-mono text-[0.75rem];
|
@apply font-mono text-[0.75rem];
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Round corners or they'll stick out of the editor bounds of editor is rounded.
|
* 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
|
* Could potentially be pushed up from the editor like we do with bg color but this
|
||||||
* is probably fine.
|
* is probably fine.
|
||||||
*/
|
*/
|
||||||
@apply rounded-lg;
|
@apply rounded-lg;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -167,8 +175,8 @@
|
|||||||
@apply h-full flex items-center;
|
@apply h-full flex items-center;
|
||||||
|
|
||||||
/* Break characters on line wrapping mode, useful for URL field.
|
/* 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 {
|
&.cm-lineWrapping {
|
||||||
@apply break-all;
|
@apply break-all;
|
||||||
@@ -176,9 +184,30 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cm-tooltip.cm-tooltip-hover {
|
||||||
|
@apply shadow-lg bg-gray-200 rounded text-gray-700 border border-gray-500 z-50 pointer-events-auto text-xs;
|
||||||
|
@apply px-2 py-1;
|
||||||
|
|
||||||
|
a {
|
||||||
|
@apply text-yellow-500 font-bold;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
@apply underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
@apply text-yellow-600 bg-yellow-600 h-3 w-3 ml-1;
|
||||||
|
content: '';
|
||||||
|
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100' fill='black' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M8.636 3.5a.5.5 0 0 0-.5-.5H1.5A1.5 1.5 0 0 0 0 4.5v10A1.5 1.5 0 0 0 1.5 16h10a1.5 1.5 0 0 0 1.5-1.5V7.864a.5.5 0 0 0-1 0V14.5a.5.5 0 0 1-.5.5h-10a.5.5 0 0 1-.5-.5v-10a.5.5 0 0 1 .5-.5h6.636a.5.5 0 0 0 .5-.5z'/%3E%3Cpath fill-rule='evenodd' d='M16 .5a.5.5 0 0 0-.5-.5h-5a.5.5 0 0 0 0 1h3.793L6.146 9.146a.5.5 0 1 0 .708.708L15 1.707V5.5a.5.5 0 0 0 1 0v-5z'/%3E%3C/svg%3E");
|
||||||
|
-webkit-mask-size: contain;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* NOTE: Extra selector required to override default styles */
|
/* NOTE: Extra selector required to override default styles */
|
||||||
.cm-tooltip.cm-tooltip {
|
.cm-tooltip.cm-tooltip-autocomplete {
|
||||||
@apply shadow-lg bg-gray-50 rounded text-gray-700 border border-gray-200 z-50 pointer-events-auto text-[0.75rem];
|
@apply shadow-lg bg-gray-50 rounded text-gray-700 border border-gray-300 z-50 pointer-events-auto text-xs;
|
||||||
|
|
||||||
.cm-completionIcon {
|
.cm-completionIcon {
|
||||||
@apply italic font-mono;
|
@apply italic font-mono;
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
cloneElement,
|
cloneElement,
|
||||||
forwardRef,
|
forwardRef,
|
||||||
isValidElement,
|
isValidElement,
|
||||||
memo,
|
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
useImperativeHandle,
|
useImperativeHandle,
|
||||||
@@ -57,7 +56,7 @@ export interface EditorProps {
|
|||||||
actions?: ReactNode;
|
actions?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const _Editor = forwardRef<EditorView | undefined, EditorProps>(function Editor(
|
export const Editor = forwardRef<EditorView | undefined, EditorProps>(function Editor(
|
||||||
{
|
{
|
||||||
readOnly,
|
readOnly,
|
||||||
type = 'text',
|
type = 'text',
|
||||||
@@ -179,7 +178,9 @@ const _Editor = forwardRef<EditorView | undefined, EditorProps>(function Editor(
|
|||||||
doc: `${defaultValue ?? ''}`,
|
doc: `${defaultValue ?? ''}`,
|
||||||
extensions: [
|
extensions: [
|
||||||
languageCompartment.of(langExt),
|
languageCompartment.of(langExt),
|
||||||
placeholderCompartment.current.of([]),
|
placeholderCompartment.current.of(
|
||||||
|
placeholderExt(placeholderElFromText(placeholder ?? '')),
|
||||||
|
),
|
||||||
wrapLinesCompartment.current.of([]),
|
wrapLinesCompartment.current.of([]),
|
||||||
...getExtensions({
|
...getExtensions({
|
||||||
container,
|
container,
|
||||||
@@ -293,8 +294,6 @@ const _Editor = forwardRef<EditorView | undefined, EditorProps>(function Editor(
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
export const Editor = memo(_Editor);
|
|
||||||
|
|
||||||
function getExtensions({
|
function getExtensions({
|
||||||
container,
|
container,
|
||||||
readOnly,
|
readOnly,
|
||||||
|
|||||||
@@ -122,11 +122,8 @@ export const baseExtensions = [
|
|||||||
history(),
|
history(),
|
||||||
dropCursor(),
|
dropCursor(),
|
||||||
drawSelection(),
|
drawSelection(),
|
||||||
// TODO: Figure out how to debounce showing of autocomplete in a good way
|
|
||||||
// debouncedAutocompletionDisplay({ millis: 1000 }),
|
|
||||||
// autocompletion({ closeOnBlur: true, interactionDelay: 200, activateOnTyping: false }),
|
|
||||||
autocompletion({
|
autocompletion({
|
||||||
closeOnBlur: false, // For debugging in devtools without closing it
|
closeOnBlur: true, // Set to `false` for debugging in devtools without closing it
|
||||||
compareCompletions: (a, b) => {
|
compareCompletions: (a, b) => {
|
||||||
// Don't sort completions at all, only on boost
|
// Don't sort completions at all, only on boost
|
||||||
return (a.boost ?? 0) - (b.boost ?? 0);
|
return (a.boost ?? 0) - (b.boost ?? 0);
|
||||||
|
|||||||
98
src-web/components/core/Editor/hyperlink/extension.ts
Normal file
98
src-web/components/core/Editor/hyperlink/extension.ts
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import type { DecorationSet, ViewUpdate } from '@codemirror/view';
|
||||||
|
import { Decoration, hoverTooltip, MatchDecorator, ViewPlugin } from '@codemirror/view';
|
||||||
|
import { EditorView } from 'codemirror';
|
||||||
|
|
||||||
|
const REGEX =
|
||||||
|
/(https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&/=]*))/g;
|
||||||
|
|
||||||
|
const tooltip = hoverTooltip(
|
||||||
|
(view, pos, side) => {
|
||||||
|
const { from, text } = view.state.doc.lineAt(pos);
|
||||||
|
let match;
|
||||||
|
let found: { start: number; end: number } | null = null;
|
||||||
|
|
||||||
|
while ((match = REGEX.exec(text))) {
|
||||||
|
const start = from + match.index;
|
||||||
|
const end = start + match[0].length;
|
||||||
|
|
||||||
|
if (pos >= start && pos <= end) {
|
||||||
|
found = { start, end };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (found == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((found.start == pos && side < 0) || (found.end == pos && side > 0)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
pos: found.start,
|
||||||
|
end: found.end,
|
||||||
|
create() {
|
||||||
|
const dom = document.createElement('a');
|
||||||
|
dom.textContent = 'Open in browser';
|
||||||
|
dom.href = text.substring(found!.start - from, found!.end - from);
|
||||||
|
dom.target = '_blank';
|
||||||
|
dom.rel = 'noopener noreferrer';
|
||||||
|
return { dom };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hoverTime: 100,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const decorator = function () {
|
||||||
|
const placeholderMatcher = new MatchDecorator({
|
||||||
|
regexp: REGEX,
|
||||||
|
decoration(match, view, matchStartPos) {
|
||||||
|
const matchEndPos = matchStartPos + match[0].length - 1;
|
||||||
|
|
||||||
|
// Don't decorate if the cursor is inside the match
|
||||||
|
for (const r of view.state.selection.ranges) {
|
||||||
|
if (r.from > matchStartPos && r.to <= matchEndPos) {
|
||||||
|
return Decoration.replace({});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const groupMatch = match[1];
|
||||||
|
if (groupMatch == null) {
|
||||||
|
// Should never happen, but make TS happy
|
||||||
|
console.warn('Group match was empty', match);
|
||||||
|
return Decoration.replace({});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Decoration.mark({
|
||||||
|
class: 'hyperlink-widget',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return ViewPlugin.fromClass(
|
||||||
|
class {
|
||||||
|
placeholders: DecorationSet;
|
||||||
|
|
||||||
|
constructor(view: EditorView) {
|
||||||
|
this.placeholders = placeholderMatcher.createDeco(view);
|
||||||
|
}
|
||||||
|
|
||||||
|
update(update: ViewUpdate) {
|
||||||
|
this.placeholders = placeholderMatcher.updateDeco(update, this.placeholders);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
decorations: (instance) => instance.placeholders,
|
||||||
|
provide: (plugin) =>
|
||||||
|
EditorView.bidiIsolatedRanges.of((view) => {
|
||||||
|
return view.plugin(plugin)?.placeholders || Decoration.none;
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const hyperlink = [tooltip, decorator()];
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
import type { DecorationSet, ViewUpdate } from '@codemirror/view';
|
import type { DecorationSet, ViewUpdate } from '@codemirror/view';
|
||||||
import { Decoration, EditorView, MatchDecorator, ViewPlugin, WidgetType } from '@codemirror/view';
|
import { Decoration, EditorView, ViewPlugin, WidgetType } from '@codemirror/view';
|
||||||
|
import { BetterMatchDecorator } from '../BetterMatchDecorator';
|
||||||
|
|
||||||
class PlaceholderWidget extends WidgetType {
|
class PlaceholderWidget extends WidgetType {
|
||||||
constructor(
|
constructor(readonly name: string, readonly isExistingVariable: boolean) {
|
||||||
readonly name: string,
|
|
||||||
readonly isExistingVariable: boolean,
|
|
||||||
) {
|
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
eq(other: PlaceholderWidget) {
|
eq(other: PlaceholderWidget) {
|
||||||
@@ -25,19 +23,6 @@ class PlaceholderWidget extends WidgetType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* This is a custom MatchDecorator that will not decorate a match if the selection is inside it
|
|
||||||
*/
|
|
||||||
class BetterMatchDecorator extends MatchDecorator {
|
|
||||||
updateDeco(update: ViewUpdate, deco: DecorationSet): DecorationSet {
|
|
||||||
if (!update.startState.selection.eq(update.state.selection)) {
|
|
||||||
return super.createDeco(update.view);
|
|
||||||
} else {
|
|
||||||
return super.updateDeco(update, deco);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const placeholders = function (variables: { name: string }[]) {
|
export const placeholders = function (variables: { name: string }[]) {
|
||||||
const placeholderMatcher = new BetterMatchDecorator({
|
const placeholderMatcher = new BetterMatchDecorator({
|
||||||
regexp: /\$\{\[\s*([^\]\s]+)\s*]}/g,
|
regexp: /\$\{\[\s*([^\]\s]+)\s*]}/g,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import classNames from 'classnames';
|
|||||||
import type { EditorView } from 'codemirror';
|
import type { EditorView } from 'codemirror';
|
||||||
import type { HTMLAttributes, ReactNode } from 'react';
|
import type { HTMLAttributes, ReactNode } from 'react';
|
||||||
import { forwardRef, useCallback, useMemo, useRef, useState } from 'react';
|
import { forwardRef, useCallback, useMemo, useRef, useState } from 'react';
|
||||||
|
import { useStateSyncDefault } from '../../hooks/useStateSyncDefault';
|
||||||
import type { EditorProps } from './Editor';
|
import type { EditorProps } from './Editor';
|
||||||
import { Editor } from './Editor';
|
import { Editor } from './Editor';
|
||||||
import { IconButton } from './IconButton';
|
import { IconButton } from './IconButton';
|
||||||
@@ -69,7 +70,7 @@ export const Input = forwardRef<EditorView | undefined, InputProps>(function Inp
|
|||||||
}: InputProps,
|
}: InputProps,
|
||||||
ref,
|
ref,
|
||||||
) {
|
) {
|
||||||
const [obscured, setObscured] = useState(type === 'password');
|
const [obscured, setObscured] = useStateSyncDefault(type === 'password');
|
||||||
const [currentValue, setCurrentValue] = useState(defaultValue ?? '');
|
const [currentValue, setCurrentValue] = useState(defaultValue ?? '');
|
||||||
const [focused, setFocused] = useState(false);
|
const [focused, setFocused] = useState(false);
|
||||||
|
|
||||||
@@ -181,9 +182,10 @@ export const Input = forwardRef<EditorView | undefined, InputProps>(function Inp
|
|||||||
<IconButton
|
<IconButton
|
||||||
title={obscured ? `Show ${label}` : `Obscure ${label}`}
|
title={obscured ? `Show ${label}` : `Obscure ${label}`}
|
||||||
size="xs"
|
size="xs"
|
||||||
className="mr-0.5"
|
className="mr-0.5 group/obscure !h-auto my-0.5"
|
||||||
|
iconClassName="text-gray-500 group-hover/obscure:text-gray-800"
|
||||||
iconSize="sm"
|
iconSize="sm"
|
||||||
icon={obscured ? 'eyeClosed' : 'eye'}
|
icon={obscured ? 'eye' : 'eyeClosed'}
|
||||||
onClick={() => setObscured((o) => !o)}
|
onClick={() => setObscured((o) => !o)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -33,3 +33,7 @@ export function Link({ href, children, className, ...other }: Props) {
|
|||||||
</RouterLink>
|
</RouterLink>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function FeedbackLink() {
|
||||||
|
return <Link href="https://yaak.canny.io">Feedback</Link>;
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Fragment, memo, useCallback, useEffect, useMemo, useRef, useState } fro
|
|||||||
import type { XYCoord } from 'react-dnd';
|
import type { XYCoord } from 'react-dnd';
|
||||||
import { useDrag, useDrop } from 'react-dnd';
|
import { useDrag, useDrop } from 'react-dnd';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
|
import { usePrompt } from '../../hooks/usePrompt';
|
||||||
import { DropMarker } from '../DropMarker';
|
import { DropMarker } from '../DropMarker';
|
||||||
import { Button } from './Button';
|
import { Button } from './Button';
|
||||||
import { Checkbox } from './Checkbox';
|
import { Checkbox } from './Checkbox';
|
||||||
@@ -14,6 +15,7 @@ import { Icon } from './Icon';
|
|||||||
import { IconButton } from './IconButton';
|
import { IconButton } from './IconButton';
|
||||||
import type { InputProps } from './Input';
|
import type { InputProps } from './Input';
|
||||||
import { Input } from './Input';
|
import { Input } from './Input';
|
||||||
|
import { RadioDropdown } from './RadioDropdown';
|
||||||
|
|
||||||
export type PairEditorProps = {
|
export type PairEditorProps = {
|
||||||
pairs: Pair[];
|
pairs: Pair[];
|
||||||
@@ -22,6 +24,7 @@ export type PairEditorProps = {
|
|||||||
className?: string;
|
className?: string;
|
||||||
namePlaceholder?: string;
|
namePlaceholder?: string;
|
||||||
valuePlaceholder?: string;
|
valuePlaceholder?: string;
|
||||||
|
valueType?: 'text' | 'password';
|
||||||
nameAutocomplete?: GenericCompletionConfig;
|
nameAutocomplete?: GenericCompletionConfig;
|
||||||
valueAutocomplete?: (name: string) => GenericCompletionConfig | undefined;
|
valueAutocomplete?: (name: string) => GenericCompletionConfig | undefined;
|
||||||
nameAutocompleteVariables?: boolean;
|
nameAutocompleteVariables?: boolean;
|
||||||
@@ -36,6 +39,7 @@ export type Pair = {
|
|||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
value: string;
|
value: string;
|
||||||
|
contentType?: string;
|
||||||
isFile?: boolean;
|
isFile?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -51,6 +55,7 @@ export const PairEditor = memo(function PairEditor({
|
|||||||
nameAutocompleteVariables,
|
nameAutocompleteVariables,
|
||||||
namePlaceholder,
|
namePlaceholder,
|
||||||
nameValidate,
|
nameValidate,
|
||||||
|
valueType,
|
||||||
onChange,
|
onChange,
|
||||||
pairs: originalPairs,
|
pairs: originalPairs,
|
||||||
valueAutocomplete,
|
valueAutocomplete,
|
||||||
@@ -176,6 +181,7 @@ export const PairEditor = memo(function PairEditor({
|
|||||||
allowFileValues={allowFileValues}
|
allowFileValues={allowFileValues}
|
||||||
nameAutocompleteVariables={nameAutocompleteVariables}
|
nameAutocompleteVariables={nameAutocompleteVariables}
|
||||||
valueAutocompleteVariables={valueAutocompleteVariables}
|
valueAutocompleteVariables={valueAutocompleteVariables}
|
||||||
|
valueType={valueType}
|
||||||
forceFocusPairId={forceFocusPairId}
|
forceFocusPairId={forceFocusPairId}
|
||||||
forceUpdateKey={forceUpdateKey}
|
forceUpdateKey={forceUpdateKey}
|
||||||
nameAutocomplete={nameAutocomplete}
|
nameAutocomplete={nameAutocomplete}
|
||||||
@@ -218,6 +224,7 @@ type FormRowProps = {
|
|||||||
| 'valueAutocomplete'
|
| 'valueAutocomplete'
|
||||||
| 'nameAutocompleteVariables'
|
| 'nameAutocompleteVariables'
|
||||||
| 'valueAutocompleteVariables'
|
| 'valueAutocompleteVariables'
|
||||||
|
| 'valueType'
|
||||||
| 'namePlaceholder'
|
| 'namePlaceholder'
|
||||||
| 'valuePlaceholder'
|
| 'valuePlaceholder'
|
||||||
| 'nameValidate'
|
| 'nameValidate'
|
||||||
@@ -246,9 +253,11 @@ const FormRow = memo(function FormRow({
|
|||||||
valueAutocompleteVariables,
|
valueAutocompleteVariables,
|
||||||
valuePlaceholder,
|
valuePlaceholder,
|
||||||
valueValidate,
|
valueValidate,
|
||||||
|
valueType,
|
||||||
}: FormRowProps) {
|
}: FormRowProps) {
|
||||||
const { id } = pairContainer;
|
const { id } = pairContainer;
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
const prompt = usePrompt();
|
||||||
const nameInputRef = useRef<EditorView>(null);
|
const nameInputRef = useRef<EditorView>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -278,6 +287,11 @@ const FormRow = memo(function FormRow({
|
|||||||
[onChange, id, pairContainer.pair],
|
[onChange, id, pairContainer.pair],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleChangeValueContentType = useMemo(
|
||||||
|
() => (contentType: string) => onChange({ id, pair: { ...pairContainer.pair, contentType } }),
|
||||||
|
[onChange, id, pairContainer.pair],
|
||||||
|
);
|
||||||
|
|
||||||
const handleFocus = useCallback(() => onFocus?.(pairContainer), [onFocus, pairContainer]);
|
const handleFocus = useCallback(() => onFocus?.(pairContainer), [onFocus, pairContainer]);
|
||||||
const handleDelete = useCallback(
|
const handleDelete = useCallback(
|
||||||
() => onDelete?.(pairContainer, false),
|
() => onDelete?.(pairContainer, false),
|
||||||
@@ -397,39 +411,73 @@ const FormRow = memo(function FormRow({
|
|||||||
name="value"
|
name="value"
|
||||||
onChange={handleChangeValueText}
|
onChange={handleChangeValueText}
|
||||||
onFocus={handleFocus}
|
onFocus={handleFocus}
|
||||||
|
type={isLast ? 'text' : valueType}
|
||||||
placeholder={valuePlaceholder ?? 'value'}
|
placeholder={valuePlaceholder ?? 'value'}
|
||||||
autocomplete={valueAutocomplete?.(pairContainer.pair.name)}
|
autocomplete={valueAutocomplete?.(pairContainer.pair.name)}
|
||||||
autocompleteVariables={valueAutocompleteVariables}
|
autocompleteVariables={valueAutocompleteVariables}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{allowFileValues && (
|
|
||||||
<Dropdown
|
|
||||||
items={[
|
|
||||||
{ key: 'text', label: 'Text', onSelect: () => handleChangeValueText('') },
|
|
||||||
{ key: 'file', label: 'File', onSelect: () => handleChangeValueFile('') },
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<IconButton
|
|
||||||
iconSize="sm"
|
|
||||||
size="xs"
|
|
||||||
icon={isLast ? 'empty' : 'chevronDown'}
|
|
||||||
title="Select form data type"
|
|
||||||
/>
|
|
||||||
</Dropdown>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<IconButton
|
{allowFileValues ? (
|
||||||
aria-hidden={isLast}
|
<RadioDropdown
|
||||||
disabled={isLast}
|
value={pairContainer.pair.isFile ? 'file' : 'text'}
|
||||||
color="custom"
|
onChange={(v) => {
|
||||||
icon={!isLast ? 'trash' : 'empty'}
|
if (v === 'file') handleChangeValueFile('');
|
||||||
size="sm"
|
else handleChangeValueText('');
|
||||||
iconSize="sm"
|
}}
|
||||||
title="Delete header"
|
items={[
|
||||||
onClick={!isLast ? handleDelete : undefined}
|
{ label: 'Text', value: 'text' },
|
||||||
className="ml-0.5 opacity-0 group-hover:!opacity-100 focus-visible:!opacity-100"
|
{ label: 'File', value: 'file' },
|
||||||
/>
|
]}
|
||||||
|
extraItems={[
|
||||||
|
{
|
||||||
|
key: 'mime',
|
||||||
|
label: 'Set Content-Type',
|
||||||
|
leftSlot: <Icon icon="pencil" />,
|
||||||
|
onSelect: async () => {
|
||||||
|
const v = await prompt({
|
||||||
|
id: 'content-type',
|
||||||
|
require: false,
|
||||||
|
title: 'Override Content-Type',
|
||||||
|
label: 'Content-Type',
|
||||||
|
placeholder: 'text/plain',
|
||||||
|
defaultValue: pairContainer.pair.contentType ?? '',
|
||||||
|
name: 'content-type',
|
||||||
|
confirmLabel: 'Set',
|
||||||
|
description: 'Leave blank to auto-detect',
|
||||||
|
});
|
||||||
|
handleChangeValueContentType(v);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'delete',
|
||||||
|
label: 'Delete',
|
||||||
|
onSelect: handleDelete,
|
||||||
|
variant: 'danger',
|
||||||
|
leftSlot: <Icon icon="trash" />,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
iconSize="sm"
|
||||||
|
size="xs"
|
||||||
|
icon={isLast ? 'empty' : 'chevronDown'}
|
||||||
|
title="Select form data type"
|
||||||
|
/>
|
||||||
|
</RadioDropdown>
|
||||||
|
) : (
|
||||||
|
<Dropdown
|
||||||
|
items={[{ key: 'delete', label: 'Delete', onSelect: handleDelete, variant: 'danger' }]}
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
iconSize="sm"
|
||||||
|
size="xs"
|
||||||
|
icon={isLast ? 'empty' : 'chevronDown'}
|
||||||
|
title="Select form data type"
|
||||||
|
/>
|
||||||
|
</Dropdown>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import type { DropdownItemSeparator, DropdownProps } from './Dropdown';
|
import type { DropdownItem, DropdownItemSeparator, DropdownProps } from './Dropdown';
|
||||||
import { Dropdown } from './Dropdown';
|
import { Dropdown } from './Dropdown';
|
||||||
import { Icon } from './Icon';
|
import { Icon } from './Icon';
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ export function RadioDropdown<T = string | null>({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
...(extraItems ?? []),
|
...((extraItems ? [{ type: 'separator' }, ...extraItems] : []) as DropdownItem[]),
|
||||||
],
|
],
|
||||||
[items, extraItems, value, onChange],
|
[items, extraItems, value, onChange],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
|
import { useContentTypeFromHeaders } from '../../hooks/useContentTypeFromHeaders';
|
||||||
import { useDebouncedState } from '../../hooks/useDebouncedState';
|
import { useDebouncedState } from '../../hooks/useDebouncedState';
|
||||||
import { useFilterResponse } from '../../hooks/useFilterResponse';
|
import { useFilterResponse } from '../../hooks/useFilterResponse';
|
||||||
import { useResponseBodyText } from '../../hooks/useResponseBodyText';
|
import { useResponseBodyText } from '../../hooks/useResponseBodyText';
|
||||||
import { useResponseContentType } from '../../hooks/useResponseContentType';
|
|
||||||
import { useToggle } from '../../hooks/useToggle';
|
import { useToggle } from '../../hooks/useToggle';
|
||||||
import { tryFormatJson, tryFormatXml } from '../../lib/formatters';
|
import { tryFormatJson, tryFormatXml } from '../../lib/formatters';
|
||||||
import type { HttpResponse } from '../../lib/models';
|
import type { HttpResponse } from '../../lib/models';
|
||||||
import { Editor } from '../core/Editor';
|
import { Editor } from '../core/Editor';
|
||||||
|
import { hyperlink } from '../core/Editor/hyperlink/extension';
|
||||||
import { IconButton } from '../core/IconButton';
|
import { IconButton } from '../core/IconButton';
|
||||||
import { Input } from '../core/Input';
|
import { Input } from '../core/Input';
|
||||||
|
|
||||||
|
const extraExtensions = [hyperlink];
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
response: HttpResponse;
|
response: HttpResponse;
|
||||||
pretty: boolean;
|
pretty: boolean;
|
||||||
@@ -21,7 +24,7 @@ export function TextViewer({ response, pretty }: Props) {
|
|||||||
const [isSearching, toggleIsSearching] = useToggle();
|
const [isSearching, toggleIsSearching] = useToggle();
|
||||||
const [filterText, setDebouncedFilterText, setFilterText] = useDebouncedState<string>('', 400);
|
const [filterText, setDebouncedFilterText, setFilterText] = useDebouncedState<string>('', 400);
|
||||||
|
|
||||||
const contentType = useResponseContentType(response);
|
const contentType = useContentTypeFromHeaders(response.headers);
|
||||||
const rawBody = useResponseBodyText(response) ?? '';
|
const rawBody = useResponseBodyText(response) ?? '';
|
||||||
const formattedBody =
|
const formattedBody =
|
||||||
pretty && contentType?.includes('json')
|
pretty && contentType?.includes('json')
|
||||||
@@ -87,6 +90,7 @@ export function TextViewer({ response, pretty }: Props) {
|
|||||||
defaultValue={body}
|
defaultValue={body}
|
||||||
contentType={contentType}
|
contentType={contentType}
|
||||||
actions={actions}
|
actions={actions}
|
||||||
|
extraExtensions={extraExtensions}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export interface PromptProps {
|
|||||||
name: InputProps['name'];
|
name: InputProps['name'];
|
||||||
defaultValue: InputProps['defaultValue'];
|
defaultValue: InputProps['defaultValue'];
|
||||||
placeholder: InputProps['placeholder'];
|
placeholder: InputProps['placeholder'];
|
||||||
|
require?: InputProps['require'];
|
||||||
confirmLabel?: string;
|
confirmLabel?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,6 +23,7 @@ export function Prompt({
|
|||||||
defaultValue,
|
defaultValue,
|
||||||
placeholder,
|
placeholder,
|
||||||
onResult,
|
onResult,
|
||||||
|
require = true,
|
||||||
confirmLabel = 'Save',
|
confirmLabel = 'Save',
|
||||||
}: PromptProps) {
|
}: PromptProps) {
|
||||||
const [value, setValue] = useState<string>(defaultValue ?? '');
|
const [value, setValue] = useState<string>(defaultValue ?? '');
|
||||||
@@ -41,7 +43,7 @@ export function Prompt({
|
|||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
hideLabel
|
hideLabel
|
||||||
require
|
require={require}
|
||||||
autoSelect
|
autoSelect
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
label={label}
|
label={label}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { NAMESPACE_GLOBAL } from '../lib/keyValueStore';
|
|
||||||
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
|
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
|
||||||
import { useCookieJars } from './useCookieJars';
|
import { useCookieJars } from './useCookieJars';
|
||||||
import { useKeyValue } from './useKeyValue';
|
import { useKeyValue } from './useKeyValue';
|
||||||
@@ -9,7 +8,7 @@ export function useActiveCookieJar() {
|
|||||||
const cookieJars = useCookieJars();
|
const cookieJars = useCookieJars();
|
||||||
|
|
||||||
const kv = useKeyValue<string | null>({
|
const kv = useKeyValue<string | null>({
|
||||||
namespace: NAMESPACE_GLOBAL,
|
namespace: 'global',
|
||||||
key: ['activeCookieJar', workspaceId ?? 'n/a'],
|
key: ['activeCookieJar', workspaceId ?? 'n/a'],
|
||||||
fallback: null,
|
fallback: null,
|
||||||
});
|
});
|
||||||
|
|||||||
41
src-web/hooks/useCommands.ts
Normal file
41
src-web/hooks/useCommands.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import type { UseMutationOptions } from '@tanstack/react-query';
|
||||||
|
import { useMutation } from '@tanstack/react-query';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { createGlobalState } from 'react-use';
|
||||||
|
import type { TrackAction, TrackResource } from '../lib/analytics';
|
||||||
|
import type { Workspace } from '../lib/models';
|
||||||
|
|
||||||
|
interface CommandInstance<T, V> extends UseMutationOptions<V, unknown, T> {
|
||||||
|
track?: [TrackResource, TrackAction];
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Commands = {
|
||||||
|
'workspace.create': CommandInstance<Partial<Pick<Workspace, 'name'>>, Workspace>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useCommandState = createGlobalState<Commands>();
|
||||||
|
|
||||||
|
export function useRegisterCommand<K extends keyof Commands>(action: K, command: Commands[K]) {
|
||||||
|
const [, setState] = useCommandState();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setState((commands) => {
|
||||||
|
return { ...commands, [action]: command };
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove action when it goes out of scope
|
||||||
|
return () => {
|
||||||
|
setState((commands) => {
|
||||||
|
return { ...commands, [action]: undefined };
|
||||||
|
});
|
||||||
|
};
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [action]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCommand<K extends keyof Commands>(action: K) {
|
||||||
|
const [commands] = useCommandState();
|
||||||
|
const cmd = commands[action];
|
||||||
|
return useMutation({ ...cmd });
|
||||||
|
}
|
||||||
9
src-web/hooks/useContentTypeFromHeaders.ts
Normal file
9
src-web/hooks/useContentTypeFromHeaders.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import type { HttpHeader } from '../lib/models';
|
||||||
|
|
||||||
|
export function useContentTypeFromHeaders(headers: HttpHeader[] | null): string | null {
|
||||||
|
return useMemo(
|
||||||
|
() => headers?.find((h) => h.name.toLowerCase() === 'content-type')?.value ?? null,
|
||||||
|
[headers],
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -32,7 +32,12 @@ export function useCreateDropdownItems({
|
|||||||
label: 'GraphQL Query',
|
label: 'GraphQL Query',
|
||||||
leftSlot: hideIcons ? undefined : <Icon icon="plus" />,
|
leftSlot: hideIcons ? undefined : <Icon icon="plus" />,
|
||||||
onSelect: () =>
|
onSelect: () =>
|
||||||
createHttpRequest.mutate({ folderId, bodyType: BODY_TYPE_GRAPHQL, method: 'POST' }),
|
createHttpRequest.mutate({
|
||||||
|
folderId,
|
||||||
|
bodyType: BODY_TYPE_GRAPHQL,
|
||||||
|
method: 'POST',
|
||||||
|
headers: [{ name: 'Content-Type', value: 'application/json' }],
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'create-grpc-request',
|
key: 'create-grpc-request',
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ export function useCreateHttpRequest() {
|
|||||||
return useMutation<
|
return useMutation<
|
||||||
HttpRequest,
|
HttpRequest,
|
||||||
unknown,
|
unknown,
|
||||||
Partial<Pick<HttpRequest, 'name' | 'sortPriority' | 'folderId' | 'bodyType' | 'method'>>
|
Partial<
|
||||||
|
Pick<HttpRequest, 'name' | 'sortPriority' | 'folderId' | 'bodyType' | 'method' | 'headers'>
|
||||||
|
>
|
||||||
>({
|
>({
|
||||||
mutationFn: (patch) => {
|
mutationFn: (patch) => {
|
||||||
if (workspaceId === null) {
|
if (workspaceId === null) {
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
import { useMutation } from '@tanstack/react-query';
|
|
||||||
import { invoke } from '@tauri-apps/api';
|
import { invoke } from '@tauri-apps/api';
|
||||||
import { trackEvent } from '../lib/analytics';
|
|
||||||
import type { Workspace } from '../lib/models';
|
|
||||||
import { useAppRoutes } from './useAppRoutes';
|
import { useAppRoutes } from './useAppRoutes';
|
||||||
|
import { useRegisterCommand } from './useCommands';
|
||||||
import { usePrompt } from './usePrompt';
|
import { usePrompt } from './usePrompt';
|
||||||
|
|
||||||
export function useCreateWorkspace({ navigateAfter }: { navigateAfter: boolean }) {
|
export function useGlobalCommands() {
|
||||||
const routes = useAppRoutes();
|
|
||||||
const prompt = usePrompt();
|
const prompt = usePrompt();
|
||||||
return useMutation<Workspace, unknown, Partial<Pick<Workspace, 'name'>>>({
|
const routes = useAppRoutes();
|
||||||
|
|
||||||
|
useRegisterCommand('workspace.create', {
|
||||||
|
name: 'New Workspace',
|
||||||
|
track: ['workspace', 'create'],
|
||||||
|
onSuccess: async (workspace) => {
|
||||||
|
routes.navigate('workspace', { workspaceId: workspace.id });
|
||||||
|
},
|
||||||
mutationFn: async ({ name: patchName }) => {
|
mutationFn: async ({ name: patchName }) => {
|
||||||
const name =
|
const name =
|
||||||
patchName ??
|
patchName ??
|
||||||
@@ -23,11 +27,5 @@ export function useCreateWorkspace({ navigateAfter }: { navigateAfter: boolean }
|
|||||||
}));
|
}));
|
||||||
return invoke('cmd_create_workspace', { name });
|
return invoke('cmd_create_workspace', { name });
|
||||||
},
|
},
|
||||||
onSettled: () => trackEvent('workspace', 'create'),
|
|
||||||
onSuccess: async (workspace) => {
|
|
||||||
if (navigateAfter) {
|
|
||||||
routes.navigate('workspace', { workspaceId: workspace.id });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
import { NAMESPACE_GLOBAL } from '../lib/keyValueStore';
|
|
||||||
import { useKeyValue } from './useKeyValue';
|
import { useKeyValue } from './useKeyValue';
|
||||||
|
|
||||||
export function protoFilesArgs(requestId: string | null) {
|
export function protoFilesArgs(requestId: string | null) {
|
||||||
return {
|
return {
|
||||||
namespace: NAMESPACE_GLOBAL,
|
namespace: 'global' as const,
|
||||||
key: ['proto_files', requestId ?? 'n/a'],
|
key: ['proto_files', requestId ?? 'n/a'],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,10 +80,14 @@ export function useIntrospectGraphQL(baseRequest: HttpRequest) {
|
|||||||
setRefetchKey((k) => k + 1);
|
setRefetchKey((k) => k + 1);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const schema = useMemo(
|
const schema = useMemo(() => {
|
||||||
() => (introspection ? buildClientSchema(introspection) : undefined),
|
try {
|
||||||
[introspection],
|
return introspection ? buildClientSchema(introspection) : undefined;
|
||||||
);
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
} catch (e: any) {
|
||||||
|
setError('message' in e ? e.message : String(e));
|
||||||
|
}
|
||||||
|
}, [introspection]);
|
||||||
|
|
||||||
return { schema, isLoading, error, refetch };
|
return { schema, isLoading, error, refetch };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export function useKeyValue<T extends Object | null>({
|
|||||||
key,
|
key,
|
||||||
fallback,
|
fallback,
|
||||||
}: {
|
}: {
|
||||||
namespace?: string;
|
namespace?: 'app' | 'no_sync' | 'global';
|
||||||
key: string | string[];
|
key: string | string[];
|
||||||
fallback: T;
|
fallback: T;
|
||||||
}) {
|
}) {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export function usePrompt() {
|
|||||||
defaultValue,
|
defaultValue,
|
||||||
placeholder,
|
placeholder,
|
||||||
confirmLabel,
|
confirmLabel,
|
||||||
|
require,
|
||||||
}: Pick<DialogProps, 'title' | 'description'> &
|
}: Pick<DialogProps, 'title' | 'description'> &
|
||||||
Omit<PromptProps, 'onResult' | 'onHide'> & { id: string }) =>
|
Omit<PromptProps, 'onResult' | 'onHide'> & { id: string }) =>
|
||||||
new Promise((onResult: PromptProps['onResult']) => {
|
new Promise((onResult: PromptProps['onResult']) => {
|
||||||
@@ -24,7 +25,16 @@ export function usePrompt() {
|
|||||||
hideX: true,
|
hideX: true,
|
||||||
size: 'sm',
|
size: 'sm',
|
||||||
render: ({ hide }) =>
|
render: ({ hide }) =>
|
||||||
Prompt({ onHide: hide, onResult, name, label, defaultValue, placeholder, confirmLabel }),
|
Prompt({
|
||||||
|
onHide: hide,
|
||||||
|
onResult,
|
||||||
|
name,
|
||||||
|
label,
|
||||||
|
defaultValue,
|
||||||
|
placeholder,
|
||||||
|
confirmLabel,
|
||||||
|
require,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { useEffect, useMemo } from 'react';
|
import { useEffect, useMemo } from 'react';
|
||||||
import { getKeyValue, NAMESPACE_GLOBAL } from '../lib/keyValueStore';
|
import { getKeyValue } from '../lib/keyValueStore';
|
||||||
import { useActiveEnvironmentId } from './useActiveEnvironmentId';
|
import { useActiveEnvironmentId } from './useActiveEnvironmentId';
|
||||||
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
|
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
|
||||||
import { useEnvironments } from './useEnvironments';
|
import { useEnvironments } from './useEnvironments';
|
||||||
import { useKeyValue } from './useKeyValue';
|
import { useKeyValue } from './useKeyValue';
|
||||||
|
|
||||||
const kvKey = (workspaceId: string) => 'recent_environments::' + workspaceId;
|
const kvKey = (workspaceId: string) => 'recent_environments::' + workspaceId;
|
||||||
const namespace = NAMESPACE_GLOBAL;
|
const namespace = 'global';
|
||||||
const fallback: string[] = [];
|
const fallback: string[] = [];
|
||||||
|
|
||||||
export function useRecentEnvironments() {
|
export function useRecentEnvironments() {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useMemo } from 'react';
|
import { useEffect, useMemo } from 'react';
|
||||||
import { getKeyValue, NAMESPACE_GLOBAL } from '../lib/keyValueStore';
|
import { getKeyValue } from '../lib/keyValueStore';
|
||||||
import { useActiveRequestId } from './useActiveRequestId';
|
import { useActiveRequestId } from './useActiveRequestId';
|
||||||
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
|
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
|
||||||
import { useGrpcRequests } from './useGrpcRequests';
|
import { useGrpcRequests } from './useGrpcRequests';
|
||||||
@@ -7,7 +7,7 @@ import { useHttpRequests } from './useHttpRequests';
|
|||||||
import { useKeyValue } from './useKeyValue';
|
import { useKeyValue } from './useKeyValue';
|
||||||
|
|
||||||
const kvKey = (workspaceId: string) => 'recent_requests::' + workspaceId;
|
const kvKey = (workspaceId: string) => 'recent_requests::' + workspaceId;
|
||||||
const namespace = NAMESPACE_GLOBAL;
|
const namespace = 'global';
|
||||||
const fallback: string[] = [];
|
const fallback: string[] = [];
|
||||||
|
|
||||||
export function useRecentRequests() {
|
export function useRecentRequests() {
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { useEffect, useMemo } from 'react';
|
import { useEffect, useMemo } from 'react';
|
||||||
import { getKeyValue, NAMESPACE_GLOBAL } from '../lib/keyValueStore';
|
import { getKeyValue } from '../lib/keyValueStore';
|
||||||
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
|
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
|
||||||
import { useKeyValue } from './useKeyValue';
|
import { useKeyValue } from './useKeyValue';
|
||||||
import { useWorkspaces } from './useWorkspaces';
|
import { useWorkspaces } from './useWorkspaces';
|
||||||
|
|
||||||
const kvKey = () => 'recent_workspaces';
|
const kvKey = () => 'recent_workspaces';
|
||||||
const namespace = NAMESPACE_GLOBAL;
|
const namespace = 'global';
|
||||||
const fallback: string[] = [];
|
const fallback: string[] = [];
|
||||||
|
|
||||||
export function useRecentWorkspaces() {
|
export function useRecentWorkspaces() {
|
||||||
@@ -25,7 +25,7 @@ export function useRecentWorkspaces() {
|
|||||||
return [activeWorkspaceId, ...withoutCurrent];
|
return [activeWorkspaceId, ...withoutCurrent];
|
||||||
}).catch(console.error);
|
}).catch(console.error);
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, [activeWorkspaceId]);
|
||||||
|
|
||||||
const onlyValidIds = useMemo(
|
const onlyValidIds = useMemo(
|
||||||
() => kv.value?.filter((id) => workspaces.some((w) => w.id === id)) ?? [],
|
() => kv.value?.filter((id) => workspaces.some((w) => w.id === id)) ?? [],
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
import { useMemo } from 'react';
|
|
||||||
import type { HttpResponse } from '../lib/models';
|
|
||||||
|
|
||||||
export function useResponseContentType(response: HttpResponse | null): string | null {
|
|
||||||
return useMemo(
|
|
||||||
() => response?.headers.find((h) => h.name.toLowerCase() === 'content-type')?.value ?? null,
|
|
||||||
[response],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { NAMESPACE_NO_SYNC } from '../lib/keyValueStore';
|
|
||||||
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
|
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
|
||||||
import { useKeyValue } from './useKeyValue';
|
import { useKeyValue } from './useKeyValue';
|
||||||
|
|
||||||
export function useSidebarHidden() {
|
export function useSidebarHidden() {
|
||||||
const activeWorkspaceId = useActiveWorkspaceId();
|
const activeWorkspaceId = useActiveWorkspaceId();
|
||||||
const { set, value } = useKeyValue<boolean>({
|
const { set, value } = useKeyValue<boolean>({
|
||||||
namespace: NAMESPACE_NO_SYNC,
|
namespace: 'no_sync',
|
||||||
key: ['sidebar_hidden', activeWorkspaceId ?? 'n/a'],
|
key: ['sidebar_hidden', activeWorkspaceId ?? 'n/a'],
|
||||||
fallback: false,
|
fallback: false,
|
||||||
});
|
});
|
||||||
|
|||||||
12
src-web/hooks/useStateSyncDefault.ts
Normal file
12
src-web/hooks/useStateSyncDefault.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Like useState, except it will update the value when the default value changes
|
||||||
|
*/
|
||||||
|
export function useStateSyncDefault<T>(defaultValue: T) {
|
||||||
|
const [value, setValue] = useState(defaultValue);
|
||||||
|
useEffect(() => {
|
||||||
|
setValue(defaultValue);
|
||||||
|
}, [defaultValue]);
|
||||||
|
return [value, setValue] as const;
|
||||||
|
}
|
||||||
@@ -1,35 +1,38 @@
|
|||||||
import { invoke } from '@tauri-apps/api';
|
import { invoke } from '@tauri-apps/api';
|
||||||
|
|
||||||
export function trackEvent(
|
export type TrackResource =
|
||||||
resource:
|
| 'app'
|
||||||
| 'app'
|
| 'cookie_jar'
|
||||||
| 'cookie_jar'
|
| 'dialog'
|
||||||
| 'dialog'
|
| 'environment'
|
||||||
| 'environment'
|
| 'folder'
|
||||||
| 'folder'
|
| 'grpc_connection'
|
||||||
| 'grpc_connection'
|
| 'grpc_event'
|
||||||
| 'grpc_event'
|
| 'grpc_request'
|
||||||
| 'grpc_request'
|
| 'http_request'
|
||||||
| 'http_request'
|
| 'http_response'
|
||||||
| 'http_response'
|
| 'key_value'
|
||||||
| 'key_value'
|
| 'setting'
|
||||||
| 'setting'
|
| 'sidebar'
|
||||||
| 'sidebar'
|
| 'workspace';
|
||||||
| 'workspace',
|
|
||||||
action:
|
|
||||||
| 'cancel'
|
|
||||||
| 'commit'
|
|
||||||
| 'create'
|
|
||||||
| 'delete'
|
|
||||||
| 'delete_many'
|
|
||||||
| 'duplicate'
|
|
||||||
| 'hide'
|
|
||||||
| 'launch'
|
|
||||||
| 'send'
|
|
||||||
| 'show'
|
|
||||||
| 'toggle'
|
|
||||||
| 'update',
|
|
||||||
|
|
||||||
|
export type TrackAction =
|
||||||
|
| 'cancel'
|
||||||
|
| 'commit'
|
||||||
|
| 'create'
|
||||||
|
| 'delete'
|
||||||
|
| 'delete_many'
|
||||||
|
| 'duplicate'
|
||||||
|
| 'hide'
|
||||||
|
| 'launch'
|
||||||
|
| 'send'
|
||||||
|
| 'show'
|
||||||
|
| 'toggle'
|
||||||
|
| 'update';
|
||||||
|
|
||||||
|
export function trackEvent(
|
||||||
|
resource: TrackResource,
|
||||||
|
action: TrackAction,
|
||||||
attributes: Record<string, string | number> = {},
|
attributes: Record<string, string | number> = {},
|
||||||
) {
|
) {
|
||||||
invoke('cmd_track_event', {
|
invoke('cmd_track_event', {
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
import { invoke } from '@tauri-apps/api';
|
import { invoke } from '@tauri-apps/api';
|
||||||
import type { KeyValue } from './models';
|
import type { KeyValue } from './models';
|
||||||
|
|
||||||
export const NAMESPACE_GLOBAL = 'global';
|
|
||||||
export const NAMESPACE_NO_SYNC = 'no_sync';
|
|
||||||
|
|
||||||
export async function setKeyValue<T>({
|
export async function setKeyValue<T>({
|
||||||
namespace = NAMESPACE_GLOBAL,
|
namespace = 'global',
|
||||||
key,
|
key,
|
||||||
value,
|
value,
|
||||||
}: {
|
}: {
|
||||||
@@ -21,7 +18,7 @@ export async function setKeyValue<T>({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getKeyValue<T>({
|
export async function getKeyValue<T>({
|
||||||
namespace = NAMESPACE_GLOBAL,
|
namespace = 'global',
|
||||||
key,
|
key,
|
||||||
fallback,
|
fallback,
|
||||||
}: {
|
}: {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export const BODY_TYPE_NONE = null;
|
export const BODY_TYPE_NONE = null;
|
||||||
export const BODY_TYPE_GRAPHQL = 'graphql';
|
export const BODY_TYPE_GRAPHQL = 'graphql';
|
||||||
export const BODY_TYPE_JSON = 'application/json';
|
export const BODY_TYPE_JSON = 'application/json';
|
||||||
|
export const BODY_TYPE_BINARY = 'binary';
|
||||||
export const BODY_TYPE_OTHER = 'other';
|
export const BODY_TYPE_OTHER = 'other';
|
||||||
export const BODY_TYPE_FORM_URLENCODED = 'application/x-www-form-urlencoded';
|
export const BODY_TYPE_FORM_URLENCODED = 'application/x-www-form-urlencoded';
|
||||||
export const BODY_TYPE_FORM_MULTIPART = 'multipart/form-data';
|
export const BODY_TYPE_FORM_MULTIPART = 'multipart/form-data';
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { appWindow } from '@tauri-apps/api/window';
|
import { appWindow } from '@tauri-apps/api/window';
|
||||||
import { NAMESPACE_NO_SYNC, getKeyValue, setKeyValue } from './keyValueStore';
|
import { getKeyValue, setKeyValue } from './keyValueStore';
|
||||||
|
|
||||||
const key = ['window_pathname', appWindow.label];
|
const key = ['window_pathname', appWindow.label];
|
||||||
const namespace = NAMESPACE_NO_SYNC;
|
const namespace = 'no_sync';
|
||||||
const fallback = undefined;
|
const fallback = undefined;
|
||||||
|
|
||||||
export async function setPathname(value: string) {
|
export async function setPathname(value: string) {
|
||||||
|
|||||||
@@ -16,8 +16,9 @@
|
|||||||
font-variant-ligatures: none;
|
font-variant-ligatures: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
::selection {
|
::selection,
|
||||||
@apply bg-selection;
|
.cm-selectionBackground {
|
||||||
|
@apply bg-selection !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Disable user selection to make it more "app-like" */
|
/* Disable user selection to make it more "app-like" */
|
||||||
@@ -29,7 +30,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
a,
|
a,
|
||||||
a * {
|
a[href] * {
|
||||||
@apply cursor-pointer !important;
|
@apply cursor-pointer !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,6 +66,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.rtl {
|
||||||
|
direction: rtl;
|
||||||
|
}
|
||||||
|
|
||||||
iframe {
|
iframe {
|
||||||
&::-webkit-scrollbar-corner,
|
&::-webkit-scrollbar-corner,
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
|
|||||||
Reference in New Issue
Block a user