mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-06-07 23:22:47 +02:00
Markdown documentation for HTTP requests (#145)
This commit is contained in:
Generated
+1548
-12
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,8 @@
|
|||||||
|
ALTER TABLE http_requests
|
||||||
|
ADD COLUMN description TEXT DEFAULT '' NOT NULL;
|
||||||
|
|
||||||
|
ALTER TABLE grpc_requests
|
||||||
|
ADD COLUMN description TEXT DEFAULT '' NOT NULL;
|
||||||
|
|
||||||
|
ALTER TABLE folders
|
||||||
|
ADD COLUMN description TEXT DEFAULT '' NOT NULL;
|
||||||
@@ -903,7 +903,7 @@ async fn cmd_import_data<R: Runtime>(
|
|||||||
v.workspace_id =
|
v.workspace_id =
|
||||||
maybe_gen_id(v.workspace_id.as_str(), ModelType::TypeWorkspace, &mut id_map);
|
maybe_gen_id(v.workspace_id.as_str(), ModelType::TypeWorkspace, &mut id_map);
|
||||||
v.folder_id = maybe_gen_id_opt(v.folder_id, ModelType::TypeFolder, &mut id_map);
|
v.folder_id = maybe_gen_id_opt(v.folder_id, ModelType::TypeFolder, &mut id_map);
|
||||||
let x = upsert_grpc_request(&window, &v).await.map_err(|e| e.to_string())?;
|
let x = upsert_grpc_request(&window, v).await.map_err(|e| e.to_string())?;
|
||||||
imported_resources.grpc_requests.push(x.clone());
|
imported_resources.grpc_requests.push(x.clone());
|
||||||
}
|
}
|
||||||
info!("Imported {} grpc_requests", imported_resources.grpc_requests.len());
|
info!("Imported {} grpc_requests", imported_resources.grpc_requests.len());
|
||||||
@@ -1225,7 +1225,7 @@ async fn cmd_create_grpc_request(
|
|||||||
) -> Result<GrpcRequest, String> {
|
) -> Result<GrpcRequest, String> {
|
||||||
upsert_grpc_request(
|
upsert_grpc_request(
|
||||||
&w,
|
&w,
|
||||||
&GrpcRequest {
|
GrpcRequest {
|
||||||
workspace_id: workspace_id.to_string(),
|
workspace_id: workspace_id.to_string(),
|
||||||
name: name.to_string(),
|
name: name.to_string(),
|
||||||
folder_id: folder_id.map(|s| s.to_string()),
|
folder_id: folder_id.map(|s| s.to_string()),
|
||||||
@@ -1273,7 +1273,7 @@ async fn cmd_update_grpc_request(
|
|||||||
request: GrpcRequest,
|
request: GrpcRequest,
|
||||||
w: WebviewWindow,
|
w: WebviewWindow,
|
||||||
) -> Result<GrpcRequest, String> {
|
) -> Result<GrpcRequest, String> {
|
||||||
upsert_grpc_request(&w, &request).await.map_err(|e| e.to_string())
|
upsert_grpc_request(&w, request).await.map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export type Environment = { model: "environment", id: string, workspaceId: strin
|
|||||||
|
|
||||||
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, };
|
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, };
|
||||||
|
|
||||||
export type Folder = { model: "folder", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, name: string, sortPriority: number, };
|
export type Folder = { model: "folder", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, name: string, description: string, sortPriority: number, };
|
||||||
|
|
||||||
export type GrpcConnection = { model: "grpc_connection", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, elapsed: number, error: string | null, method: string, service: string, status: number, state: GrpcConnectionState, trailers: { [key in string]?: string }, url: string, };
|
export type GrpcConnection = { model: "grpc_connection", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, elapsed: number, error: string | null, method: string, service: string, status: number, state: GrpcConnectionState, trailers: { [key in string]?: string }, url: string, };
|
||||||
|
|
||||||
@@ -26,9 +26,9 @@ export type GrpcEventType = "info" | "error" | "client_message" | "server_messag
|
|||||||
|
|
||||||
export type GrpcMetadataEntry = { enabled?: boolean, name: string, value: string, };
|
export type GrpcMetadataEntry = { enabled?: boolean, name: string, value: string, };
|
||||||
|
|
||||||
export type GrpcRequest = { model: "grpc_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authenticationType: string | null, authentication: Record<string, any>, message: string, metadata: Array<GrpcMetadataEntry>, method: string | null, name: string, service: string | null, sortPriority: number, url: string, };
|
export type GrpcRequest = { model: "grpc_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authenticationType: string | null, authentication: Record<string, any>, description: string, message: string, metadata: Array<GrpcMetadataEntry>, method: string | null, name: string, service: string | null, sortPriority: number, url: string, };
|
||||||
|
|
||||||
export type HttpRequest = { model: "http_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, body: Record<string, any>, bodyType: string | null, headers: Array<HttpRequestHeader>, method: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, };
|
export type HttpRequest = { model: "http_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, body: Record<string, any>, bodyType: string | null, description: string, headers: Array<HttpRequestHeader>, method: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, };
|
||||||
|
|
||||||
export type HttpRequestHeader = { enabled?: boolean, name: string, value: string, };
|
export type HttpRequestHeader = { enabled?: boolean, name: string, value: string, };
|
||||||
|
|
||||||
|
|||||||
@@ -310,6 +310,7 @@ pub struct Folder {
|
|||||||
pub folder_id: Option<String>,
|
pub folder_id: Option<String>,
|
||||||
|
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
pub description: String,
|
||||||
pub sort_priority: f32,
|
pub sort_priority: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -325,6 +326,7 @@ pub enum FolderIden {
|
|||||||
UpdatedAt,
|
UpdatedAt,
|
||||||
|
|
||||||
Name,
|
Name,
|
||||||
|
Description,
|
||||||
SortPriority,
|
SortPriority,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -341,6 +343,7 @@ impl<'s> TryFrom<&Row<'s>> for Folder {
|
|||||||
updated_at: r.get("updated_at")?,
|
updated_at: r.get("updated_at")?,
|
||||||
folder_id: r.get("folder_id")?,
|
folder_id: r.get("folder_id")?,
|
||||||
name: r.get("name")?,
|
name: r.get("name")?,
|
||||||
|
description: r.get("description")?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -385,6 +388,7 @@ pub struct HttpRequest {
|
|||||||
#[ts(type = "Record<string, any>")]
|
#[ts(type = "Record<string, any>")]
|
||||||
pub body: BTreeMap<String, Value>,
|
pub body: BTreeMap<String, Value>,
|
||||||
pub body_type: Option<String>,
|
pub body_type: Option<String>,
|
||||||
|
pub description: String,
|
||||||
pub headers: Vec<HttpRequestHeader>,
|
pub headers: Vec<HttpRequestHeader>,
|
||||||
#[serde(default = "default_http_request_method")]
|
#[serde(default = "default_http_request_method")]
|
||||||
pub method: String,
|
pub method: String,
|
||||||
@@ -409,6 +413,7 @@ pub enum HttpRequestIden {
|
|||||||
AuthenticationType,
|
AuthenticationType,
|
||||||
Body,
|
Body,
|
||||||
BodyType,
|
BodyType,
|
||||||
|
Description,
|
||||||
Headers,
|
Headers,
|
||||||
Method,
|
Method,
|
||||||
Name,
|
Name,
|
||||||
@@ -437,6 +442,7 @@ impl<'s> TryFrom<&Row<'s>> for HttpRequest {
|
|||||||
method: r.get("method")?,
|
method: r.get("method")?,
|
||||||
body: serde_json::from_str(body.as_str()).unwrap_or_default(),
|
body: serde_json::from_str(body.as_str()).unwrap_or_default(),
|
||||||
body_type: r.get("body_type")?,
|
body_type: r.get("body_type")?,
|
||||||
|
description: r.get("description")?,
|
||||||
authentication: serde_json::from_str(authentication.as_str()).unwrap_or_default(),
|
authentication: serde_json::from_str(authentication.as_str()).unwrap_or_default(),
|
||||||
authentication_type: r.get("authentication_type")?,
|
authentication_type: r.get("authentication_type")?,
|
||||||
headers: serde_json::from_str(headers.as_str()).unwrap_or_default(),
|
headers: serde_json::from_str(headers.as_str()).unwrap_or_default(),
|
||||||
@@ -584,6 +590,7 @@ pub struct GrpcRequest {
|
|||||||
pub authentication_type: Option<String>,
|
pub authentication_type: Option<String>,
|
||||||
#[ts(type = "Record<string, any>")]
|
#[ts(type = "Record<string, any>")]
|
||||||
pub authentication: BTreeMap<String, Value>,
|
pub authentication: BTreeMap<String, Value>,
|
||||||
|
pub description: String,
|
||||||
pub message: String,
|
pub message: String,
|
||||||
pub metadata: Vec<GrpcMetadataEntry>,
|
pub metadata: Vec<GrpcMetadataEntry>,
|
||||||
pub method: Option<String>,
|
pub method: Option<String>,
|
||||||
@@ -606,6 +613,7 @@ pub enum GrpcRequestIden {
|
|||||||
|
|
||||||
Authentication,
|
Authentication,
|
||||||
AuthenticationType,
|
AuthenticationType,
|
||||||
|
Description,
|
||||||
Message,
|
Message,
|
||||||
Metadata,
|
Metadata,
|
||||||
Method,
|
Method,
|
||||||
@@ -629,6 +637,7 @@ impl<'s> TryFrom<&Row<'s>> for GrpcRequest {
|
|||||||
updated_at: r.get("updated_at")?,
|
updated_at: r.get("updated_at")?,
|
||||||
folder_id: r.get("folder_id")?,
|
folder_id: r.get("folder_id")?,
|
||||||
name: r.get("name")?,
|
name: r.get("name")?,
|
||||||
|
description: r.get("description")?,
|
||||||
service: r.get("service")?,
|
service: r.get("service")?,
|
||||||
method: r.get("method")?,
|
method: r.get("method")?,
|
||||||
message: r.get("message")?,
|
message: r.get("message")?,
|
||||||
|
|||||||
@@ -307,7 +307,7 @@ pub async fn duplicate_grpc_request<R: Runtime>(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
request.id = "".to_string();
|
request.id = "".to_string();
|
||||||
upsert_grpc_request(window, &request).await
|
upsert_grpc_request(window, request).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn delete_grpc_request<R: Runtime>(
|
pub async fn delete_grpc_request<R: Runtime>(
|
||||||
@@ -334,7 +334,7 @@ pub async fn delete_grpc_request<R: Runtime>(
|
|||||||
|
|
||||||
pub async fn upsert_grpc_request<R: Runtime>(
|
pub async fn upsert_grpc_request<R: Runtime>(
|
||||||
window: &WebviewWindow<R>,
|
window: &WebviewWindow<R>,
|
||||||
request: &GrpcRequest,
|
request: GrpcRequest,
|
||||||
) -> Result<GrpcRequest> {
|
) -> Result<GrpcRequest> {
|
||||||
let id = match request.id.as_str() {
|
let id = match request.id.as_str() {
|
||||||
"" => generate_model_id(ModelType::TypeGrpcRequest),
|
"" => generate_model_id(ModelType::TypeGrpcRequest),
|
||||||
@@ -351,6 +351,7 @@ pub async fn upsert_grpc_request<R: Runtime>(
|
|||||||
GrpcRequestIden::CreatedAt,
|
GrpcRequestIden::CreatedAt,
|
||||||
GrpcRequestIden::UpdatedAt,
|
GrpcRequestIden::UpdatedAt,
|
||||||
GrpcRequestIden::Name,
|
GrpcRequestIden::Name,
|
||||||
|
GrpcRequestIden::Description,
|
||||||
GrpcRequestIden::WorkspaceId,
|
GrpcRequestIden::WorkspaceId,
|
||||||
GrpcRequestIden::FolderId,
|
GrpcRequestIden::FolderId,
|
||||||
GrpcRequestIden::SortPriority,
|
GrpcRequestIden::SortPriority,
|
||||||
@@ -363,17 +364,18 @@ pub async fn upsert_grpc_request<R: Runtime>(
|
|||||||
GrpcRequestIden::Metadata,
|
GrpcRequestIden::Metadata,
|
||||||
])
|
])
|
||||||
.values_panic([
|
.values_panic([
|
||||||
id.as_str().into(),
|
id.into(),
|
||||||
CurrentTimestamp.into(),
|
CurrentTimestamp.into(),
|
||||||
CurrentTimestamp.into(),
|
CurrentTimestamp.into(),
|
||||||
trimmed_name.into(),
|
trimmed_name.into(),
|
||||||
request.workspace_id.as_str().into(),
|
request.description.into(),
|
||||||
|
request.workspace_id.into(),
|
||||||
request.folder_id.as_ref().map(|s| s.as_str()).into(),
|
request.folder_id.as_ref().map(|s| s.as_str()).into(),
|
||||||
request.sort_priority.into(),
|
request.sort_priority.into(),
|
||||||
request.url.as_str().into(),
|
request.url.into(),
|
||||||
request.service.as_ref().map(|s| s.as_str()).into(),
|
request.service.as_ref().map(|s| s.as_str()).into(),
|
||||||
request.method.as_ref().map(|s| s.as_str()).into(),
|
request.method.as_ref().map(|s| s.as_str()).into(),
|
||||||
request.message.as_str().into(),
|
request.message.into(),
|
||||||
request.authentication_type.as_ref().map(|s| s.as_str()).into(),
|
request.authentication_type.as_ref().map(|s| s.as_str()).into(),
|
||||||
serde_json::to_string(&request.authentication)?.into(),
|
serde_json::to_string(&request.authentication)?.into(),
|
||||||
serde_json::to_string(&request.metadata)?.into(),
|
serde_json::to_string(&request.metadata)?.into(),
|
||||||
@@ -384,6 +386,7 @@ pub async fn upsert_grpc_request<R: Runtime>(
|
|||||||
GrpcRequestIden::UpdatedAt,
|
GrpcRequestIden::UpdatedAt,
|
||||||
GrpcRequestIden::WorkspaceId,
|
GrpcRequestIden::WorkspaceId,
|
||||||
GrpcRequestIden::Name,
|
GrpcRequestIden::Name,
|
||||||
|
GrpcRequestIden::Description,
|
||||||
GrpcRequestIden::FolderId,
|
GrpcRequestIden::FolderId,
|
||||||
GrpcRequestIden::SortPriority,
|
GrpcRequestIden::SortPriority,
|
||||||
GrpcRequestIden::Url,
|
GrpcRequestIden::Url,
|
||||||
@@ -1064,6 +1067,7 @@ pub async fn upsert_folder<R: Runtime>(window: &WebviewWindow<R>, r: Folder) ->
|
|||||||
FolderIden::WorkspaceId,
|
FolderIden::WorkspaceId,
|
||||||
FolderIden::FolderId,
|
FolderIden::FolderId,
|
||||||
FolderIden::Name,
|
FolderIden::Name,
|
||||||
|
FolderIden::Description,
|
||||||
FolderIden::SortPriority,
|
FolderIden::SortPriority,
|
||||||
])
|
])
|
||||||
.values_panic([
|
.values_panic([
|
||||||
@@ -1073,6 +1077,7 @@ pub async fn upsert_folder<R: Runtime>(window: &WebviewWindow<R>, r: Folder) ->
|
|||||||
r.workspace_id.as_str().into(),
|
r.workspace_id.as_str().into(),
|
||||||
r.folder_id.as_ref().map(|s| s.as_str()).into(),
|
r.folder_id.as_ref().map(|s| s.as_str()).into(),
|
||||||
trimmed_name.into(),
|
trimmed_name.into(),
|
||||||
|
r.description.into(),
|
||||||
r.sort_priority.into(),
|
r.sort_priority.into(),
|
||||||
])
|
])
|
||||||
.on_conflict(
|
.on_conflict(
|
||||||
@@ -1080,6 +1085,7 @@ pub async fn upsert_folder<R: Runtime>(window: &WebviewWindow<R>, r: Folder) ->
|
|||||||
.update_columns([
|
.update_columns([
|
||||||
FolderIden::UpdatedAt,
|
FolderIden::UpdatedAt,
|
||||||
FolderIden::Name,
|
FolderIden::Name,
|
||||||
|
FolderIden::Description,
|
||||||
FolderIden::FolderId,
|
FolderIden::FolderId,
|
||||||
FolderIden::SortPriority,
|
FolderIden::SortPriority,
|
||||||
])
|
])
|
||||||
@@ -1127,6 +1133,7 @@ pub async fn upsert_http_request<R: Runtime>(
|
|||||||
HttpRequestIden::WorkspaceId,
|
HttpRequestIden::WorkspaceId,
|
||||||
HttpRequestIden::FolderId,
|
HttpRequestIden::FolderId,
|
||||||
HttpRequestIden::Name,
|
HttpRequestIden::Name,
|
||||||
|
HttpRequestIden::Description,
|
||||||
HttpRequestIden::Url,
|
HttpRequestIden::Url,
|
||||||
HttpRequestIden::UrlParameters,
|
HttpRequestIden::UrlParameters,
|
||||||
HttpRequestIden::Method,
|
HttpRequestIden::Method,
|
||||||
@@ -1141,12 +1148,13 @@ pub async fn upsert_http_request<R: Runtime>(
|
|||||||
id.as_str().into(),
|
id.as_str().into(),
|
||||||
CurrentTimestamp.into(),
|
CurrentTimestamp.into(),
|
||||||
CurrentTimestamp.into(),
|
CurrentTimestamp.into(),
|
||||||
r.workspace_id.as_str().into(),
|
r.workspace_id.into(),
|
||||||
r.folder_id.as_ref().map(|s| s.as_str()).into(),
|
r.folder_id.as_ref().map(|s| s.as_str()).into(),
|
||||||
trimmed_name.into(),
|
trimmed_name.into(),
|
||||||
r.url.as_str().into(),
|
r.description.into(),
|
||||||
|
r.url.into(),
|
||||||
serde_json::to_string(&r.url_parameters)?.into(),
|
serde_json::to_string(&r.url_parameters)?.into(),
|
||||||
r.method.as_str().into(),
|
r.method.into(),
|
||||||
serde_json::to_string(&r.body)?.into(),
|
serde_json::to_string(&r.body)?.into(),
|
||||||
r.body_type.as_ref().map(|s| s.as_str()).into(),
|
r.body_type.as_ref().map(|s| s.as_str()).into(),
|
||||||
serde_json::to_string(&r.authentication)?.into(),
|
serde_json::to_string(&r.authentication)?.into(),
|
||||||
@@ -1160,6 +1168,7 @@ pub async fn upsert_http_request<R: Runtime>(
|
|||||||
HttpRequestIden::UpdatedAt,
|
HttpRequestIden::UpdatedAt,
|
||||||
HttpRequestIden::WorkspaceId,
|
HttpRequestIden::WorkspaceId,
|
||||||
HttpRequestIden::Name,
|
HttpRequestIden::Name,
|
||||||
|
HttpRequestIden::Description,
|
||||||
HttpRequestIden::FolderId,
|
HttpRequestIden::FolderId,
|
||||||
HttpRequestIden::Method,
|
HttpRequestIden::Method,
|
||||||
HttpRequestIden::Headers,
|
HttpRequestIden::Headers,
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { useFolders } from '../hooks/useFolders';
|
||||||
|
import { useUpdateAnyFolder } from '../hooks/useUpdateAnyFolder';
|
||||||
|
import { Banner } from './core/Banner';
|
||||||
|
import { PlainInput } from './core/PlainInput';
|
||||||
|
import { VStack } from './core/Stacks';
|
||||||
|
import { MarkdownEditor } from './MarkdownEditor';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
folderId: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FolderSettingsDialog({ folderId }: Props) {
|
||||||
|
const updateFolder = useUpdateAnyFolder();
|
||||||
|
const folders = useFolders();
|
||||||
|
const folder = folders.find((f) => f.id === folderId);
|
||||||
|
|
||||||
|
if (folder == null) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VStack space={3} className="pb-3">
|
||||||
|
{updateFolder.error != null && <Banner color="danger">{String(updateFolder.error)}</Banner>}
|
||||||
|
<PlainInput
|
||||||
|
label="Folder Name"
|
||||||
|
defaultValue={folder.name}
|
||||||
|
onChange={(name) => {
|
||||||
|
if (folderId == null) return;
|
||||||
|
updateFolder.mutate({ id: folderId, update: (folder) => ({ ...folder, name }) });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<MarkdownEditor
|
||||||
|
name="folder-description"
|
||||||
|
placeholder="A Markdown description of this folder."
|
||||||
|
className="min-h-[10rem] border border-border px-2"
|
||||||
|
defaultValue={folder.description}
|
||||||
|
onChange={(description) => {
|
||||||
|
if (folderId == null) return;
|
||||||
|
updateFolder.mutate({
|
||||||
|
id: folderId,
|
||||||
|
update: (folder) => ({ ...folder, description }),
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</VStack>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import useResizeObserver from '@react-hook/resize-observer';
|
import useSize from '@react-hook/size';
|
||||||
import type { GrpcMetadataEntry, GrpcRequest } from '@yaakapp-internal/models';
|
import type { GrpcMetadataEntry, GrpcRequest } from '@yaakapp-internal/models';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import type { CSSProperties } from 'react';
|
import type { CSSProperties } from 'react';
|
||||||
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
import React, { useCallback, useMemo, useRef } from 'react';
|
||||||
import { createGlobalState } from 'react-use';
|
import { useLocalStorage } from 'react-use';
|
||||||
import type { ReflectResponseService } from '../hooks/useGrpc';
|
import type { ReflectResponseService } from '../hooks/useGrpc';
|
||||||
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
|
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
|
||||||
import { useUpdateAnyGrpcRequest } from '../hooks/useUpdateAnyGrpcRequest';
|
import { useUpdateAnyGrpcRequest } from '../hooks/useUpdateAnyGrpcRequest';
|
||||||
@@ -11,6 +11,7 @@ import { AUTH_TYPE_BASIC, AUTH_TYPE_BEARER, AUTH_TYPE_NONE } from '../lib/model_
|
|||||||
import { BasicAuth } from './BasicAuth';
|
import { BasicAuth } from './BasicAuth';
|
||||||
import { BearerAuth } from './BearerAuth';
|
import { BearerAuth } from './BearerAuth';
|
||||||
import { Button } from './core/Button';
|
import { Button } from './core/Button';
|
||||||
|
import { CountBadge } from './core/CountBadge';
|
||||||
import { Icon } from './core/Icon';
|
import { Icon } from './core/Icon';
|
||||||
import { IconButton } from './core/IconButton';
|
import { IconButton } from './core/IconButton';
|
||||||
import { PairOrBulkEditor } from './core/PairOrBulkEditor';
|
import { PairOrBulkEditor } from './core/PairOrBulkEditor';
|
||||||
@@ -20,6 +21,7 @@ import type { TabItem } from './core/Tabs/Tabs';
|
|||||||
import { TabContent, Tabs } from './core/Tabs/Tabs';
|
import { TabContent, Tabs } from './core/Tabs/Tabs';
|
||||||
import { EmptyStateText } from './EmptyStateText';
|
import { EmptyStateText } from './EmptyStateText';
|
||||||
import { GrpcEditor } from './GrpcEditor';
|
import { GrpcEditor } from './GrpcEditor';
|
||||||
|
import { MarkdownEditor } from './MarkdownEditor';
|
||||||
import { UrlBar } from './UrlBar';
|
import { UrlBar } from './UrlBar';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -44,7 +46,10 @@ interface Props {
|
|||||||
services: ReflectResponseService[] | null;
|
services: ReflectResponseService[] | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const useActiveTab = createGlobalState<string>('message');
|
const TAB_MESSAGE = 'message';
|
||||||
|
const TAB_METADATA = 'metadata';
|
||||||
|
const TAB_AUTH = 'auth';
|
||||||
|
const TAB_DESCRIPTION = 'description';
|
||||||
|
|
||||||
export function GrpcConnectionSetupPane({
|
export function GrpcConnectionSetupPane({
|
||||||
style,
|
style,
|
||||||
@@ -61,14 +66,14 @@ export function GrpcConnectionSetupPane({
|
|||||||
onSend,
|
onSend,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const updateRequest = useUpdateAnyGrpcRequest();
|
const updateRequest = useUpdateAnyGrpcRequest();
|
||||||
const [activeTab, setActiveTab] = useActiveTab();
|
const [activeTabs, setActiveTabs] = useLocalStorage<Record<string, string>>(
|
||||||
|
'grpcRequestPaneActiveTabs',
|
||||||
|
{},
|
||||||
|
);
|
||||||
const { updateKey: forceUpdateKey } = useRequestUpdateKey(activeRequest.id ?? null);
|
const { updateKey: forceUpdateKey } = useRequestUpdateKey(activeRequest.id ?? null);
|
||||||
|
|
||||||
const [paneSize, setPaneSize] = useState(99999);
|
|
||||||
const urlContainerEl = useRef<HTMLDivElement>(null);
|
const urlContainerEl = useRef<HTMLDivElement>(null);
|
||||||
useResizeObserver<HTMLDivElement>(urlContainerEl.current, (entry) => {
|
const [paneWidth] = useSize(urlContainerEl.current);
|
||||||
setPaneSize(entry.contentRect.width);
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleChangeUrl = useCallback(
|
const handleChangeUrl = useCallback(
|
||||||
(url: string) => updateRequest.mutateAsync({ id: activeRequest.id, update: { url } }),
|
(url: string) => updateRequest.mutateAsync({ id: activeRequest.id, update: { url } }),
|
||||||
@@ -129,9 +134,18 @@ export function GrpcConnectionSetupPane({
|
|||||||
|
|
||||||
const tabs: TabItem[] = useMemo(
|
const tabs: TabItem[] = useMemo(
|
||||||
() => [
|
() => [
|
||||||
{ value: 'message', label: 'Message' },
|
|
||||||
{
|
{
|
||||||
value: 'auth',
|
value: TAB_DESCRIPTION,
|
||||||
|
label: (
|
||||||
|
<div className="flex items-center">
|
||||||
|
Docs
|
||||||
|
{activeRequest.description && <CountBadge count={true} />}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{ value: TAB_MESSAGE, label: 'Message' },
|
||||||
|
{
|
||||||
|
value: TAB_AUTH,
|
||||||
label: 'Auth',
|
label: 'Auth',
|
||||||
options: {
|
options: {
|
||||||
value: activeRequest.authenticationType,
|
value: activeRequest.authenticationType,
|
||||||
@@ -160,29 +174,44 @@ export function GrpcConnectionSetupPane({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ value: 'metadata', label: 'Metadata' },
|
{ value: TAB_METADATA, label: 'Metadata' },
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
activeRequest.authentication,
|
activeRequest.authentication,
|
||||||
activeRequest.authenticationType,
|
activeRequest.authenticationType,
|
||||||
|
activeRequest.description,
|
||||||
activeRequest.id,
|
activeRequest.id,
|
||||||
updateRequest,
|
updateRequest,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const activeTab = activeTabs?.[activeRequest.id];
|
||||||
|
const setActiveTab = useCallback(
|
||||||
|
(tab: string) => {
|
||||||
|
setActiveTabs((r) => ({ ...r, [activeRequest.id]: tab }));
|
||||||
|
},
|
||||||
|
[activeRequest.id, setActiveTabs],
|
||||||
|
);
|
||||||
|
|
||||||
const handleMetadataChange = useCallback(
|
const handleMetadataChange = useCallback(
|
||||||
(metadata: GrpcMetadataEntry[]) =>
|
(metadata: GrpcMetadataEntry[]) =>
|
||||||
updateRequest.mutate({ id: activeRequest.id, update: { metadata } }),
|
updateRequest.mutate({ id: activeRequest.id, update: { metadata } }),
|
||||||
[activeRequest.id, updateRequest],
|
[activeRequest.id, updateRequest],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleDescriptionChange = useCallback(
|
||||||
|
(description: string) =>
|
||||||
|
updateRequest.mutate({ id: activeRequest.id, update: { description } }),
|
||||||
|
[activeRequest.id, updateRequest],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VStack style={style}>
|
<VStack style={style}>
|
||||||
<div
|
<div
|
||||||
ref={urlContainerEl}
|
ref={urlContainerEl}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'grid grid-cols-[minmax(0,1fr)_auto] gap-1.5',
|
'grid grid-cols-[minmax(0,1fr)_auto] gap-1.5',
|
||||||
paneSize < 400 && '!grid-cols-1',
|
paneWidth < 400 && '!grid-cols-1',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<UrlBar
|
<UrlBar
|
||||||
@@ -222,7 +251,7 @@ export function GrpcConnectionSetupPane({
|
|||||||
disabled={isStreaming || services == null}
|
disabled={isStreaming || services == null}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'font-mono text-editor min-w-[5rem] !ring-0',
|
'font-mono text-editor min-w-[5rem] !ring-0',
|
||||||
paneSize < 400 && 'flex-1',
|
paneWidth < 400 && 'flex-1',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{select.options.find((o) => o.value === select.value)?.label ?? 'No Schema'}
|
{select.options.find((o) => o.value === select.value)?.label ?? 'No Schema'}
|
||||||
@@ -312,6 +341,14 @@ export function GrpcConnectionSetupPane({
|
|||||||
forceUpdateKey={forceUpdateKey}
|
forceUpdateKey={forceUpdateKey}
|
||||||
/>
|
/>
|
||||||
</TabContent>
|
</TabContent>
|
||||||
|
<TabContent value={TAB_DESCRIPTION}>
|
||||||
|
<MarkdownEditor
|
||||||
|
name="request-description"
|
||||||
|
placeholder="A Markdown description of this request."
|
||||||
|
defaultValue={activeRequest.description}
|
||||||
|
onChange={handleDescriptionChange}
|
||||||
|
/>
|
||||||
|
</TabContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</VStack>
|
</VStack>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
import useSize from '@react-hook/size';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { useRef } from 'react';
|
||||||
|
import Markdown from 'react-markdown';
|
||||||
|
import remarkGfm from 'remark-gfm';
|
||||||
|
import { useKeyValue } from '../hooks/useKeyValue';
|
||||||
|
import { Editor } from './core/Editor';
|
||||||
|
import { IconButton } from './core/IconButton';
|
||||||
|
import { SplitLayout } from './core/SplitLayout';
|
||||||
|
import { VStack } from './core/Stacks';
|
||||||
|
import { Prose } from './Prose';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
placeholder: string;
|
||||||
|
className?: string;
|
||||||
|
defaultValue: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MarkdownEditor({ className, defaultValue, onChange, name, placeholder }: Props) {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const [width] = useSize(containerRef.current);
|
||||||
|
const wideEnoughForSplit = width > 600;
|
||||||
|
|
||||||
|
const { set: setViewMode, value: rawViewMode } = useKeyValue<'edit' | 'preview' | 'both'>({
|
||||||
|
namespace: 'global',
|
||||||
|
key: ['md_view', name],
|
||||||
|
fallback: 'edit',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (rawViewMode == null) return null;
|
||||||
|
|
||||||
|
let viewMode = rawViewMode;
|
||||||
|
if (rawViewMode === 'both' && !wideEnoughForSplit) {
|
||||||
|
viewMode = 'edit';
|
||||||
|
}
|
||||||
|
|
||||||
|
const editor = (
|
||||||
|
<Editor
|
||||||
|
className="max-w-xl"
|
||||||
|
language="markdown"
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
onChange={onChange}
|
||||||
|
placeholder={placeholder}
|
||||||
|
hideGutter
|
||||||
|
wrapLines
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const preview =
|
||||||
|
defaultValue.length === 0 ? (
|
||||||
|
<p className="text-text-subtle">No description</p>
|
||||||
|
) : (
|
||||||
|
<Prose className="max-w-xl">
|
||||||
|
<Markdown
|
||||||
|
remarkPlugins={[remarkGfm]}
|
||||||
|
components={{
|
||||||
|
a: ({ href, children, ...rest }) => {
|
||||||
|
if (href && !href.match(/https?:\/\//)) {
|
||||||
|
href = `http://${href}`;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<a target="_blank" rel="noreferrer noopener" href={href} {...rest}>
|
||||||
|
{children}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{defaultValue}
|
||||||
|
</Markdown>
|
||||||
|
</Prose>
|
||||||
|
);
|
||||||
|
|
||||||
|
const contents =
|
||||||
|
viewMode === 'both' ? (
|
||||||
|
<SplitLayout
|
||||||
|
name="markdown-editor"
|
||||||
|
layout="horizontal"
|
||||||
|
firstSlot={({ style }) => <div style={style}>{editor}</div>}
|
||||||
|
secondSlot={({ style }) => (
|
||||||
|
<div style={style} className="border-l border-border-subtle pl-6">
|
||||||
|
{preview}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
) : viewMode === 'preview' ? (
|
||||||
|
preview
|
||||||
|
) : (
|
||||||
|
editor
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className={classNames(
|
||||||
|
'w-full h-full pt-1.5 group rounded-md grid grid-cols-[minmax(0,1fr)_auto]',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="pr-8 h-full w-full">{contents}</div>
|
||||||
|
<VStack
|
||||||
|
space={1}
|
||||||
|
className="bg-surface opacity-20 group-hover:opacity-100 transition-opacity transform-gpu"
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
size="xs"
|
||||||
|
icon="text"
|
||||||
|
title="Switch to edit mode"
|
||||||
|
className={classNames(viewMode === 'edit' && 'bg-surface-highlight !text-text')}
|
||||||
|
event={{ id: 'md_mode', mode: viewMode }}
|
||||||
|
onClick={() => setViewMode('edit')}
|
||||||
|
/>
|
||||||
|
{wideEnoughForSplit && (
|
||||||
|
<IconButton
|
||||||
|
size="xs"
|
||||||
|
icon="columns_2"
|
||||||
|
title="Switch to edit mode"
|
||||||
|
className={classNames(viewMode === 'both' && 'bg-surface-highlight !text-text')}
|
||||||
|
event={{ id: 'md_mode', mode: viewMode }}
|
||||||
|
onClick={() => setViewMode('both')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<IconButton
|
||||||
|
size="xs"
|
||||||
|
icon="eye"
|
||||||
|
title="Switch to preview mode"
|
||||||
|
className={classNames(viewMode === 'preview' && 'bg-surface-highlight !text-text')}
|
||||||
|
event={{ id: 'md_mode', mode: viewMode }}
|
||||||
|
onClick={() => setViewMode('preview')}
|
||||||
|
/>
|
||||||
|
</VStack>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,201 @@
|
|||||||
|
.prose {
|
||||||
|
@apply text-text;
|
||||||
|
|
||||||
|
& > :first-child {
|
||||||
|
@apply mt-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
img,
|
||||||
|
video,
|
||||||
|
p,
|
||||||
|
ul,
|
||||||
|
ol,
|
||||||
|
table,
|
||||||
|
blockquote,
|
||||||
|
hr,
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
@apply my-5;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
@apply mt-10 leading-tight text-balance;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
@apply text-pretty;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
@apply text-4xl font-bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
@apply text-2xl font-bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
@apply text-xl font-bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
em {
|
||||||
|
@apply italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
strong {
|
||||||
|
@apply font-bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
@apply list-disc;
|
||||||
|
|
||||||
|
ul, ol {
|
||||||
|
@apply my-0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ol {
|
||||||
|
@apply list-decimal;
|
||||||
|
|
||||||
|
ol, ul {
|
||||||
|
@apply my-0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ol, ul {
|
||||||
|
@apply pl-6;
|
||||||
|
|
||||||
|
li p {
|
||||||
|
@apply inline-block my-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
@apply pl-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
li::marker {
|
||||||
|
@apply text-success;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
@apply text-notice hover:underline;
|
||||||
|
|
||||||
|
* {
|
||||||
|
@apply text-notice !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
img,
|
||||||
|
video {
|
||||||
|
@apply max-h-[65vh];
|
||||||
|
@apply w-auto mx-auto rounded-md;
|
||||||
|
}
|
||||||
|
|
||||||
|
table code,
|
||||||
|
p code,
|
||||||
|
ol code,
|
||||||
|
ul code {
|
||||||
|
@apply text-xs bg-surface-active text-info font-normal whitespace-nowrap;
|
||||||
|
@apply px-1.5 py-0.5 rounded not-italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
@apply bg-surface-highlight text-text !important;
|
||||||
|
@apply px-4 py-3 rounded-md;
|
||||||
|
@apply overflow-auto whitespace-pre;
|
||||||
|
|
||||||
|
code {
|
||||||
|
@apply text-xs font-normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner {
|
||||||
|
@apply border border-dashed;
|
||||||
|
@apply border-border bg-surface-highlight text-text px-4 py-3 rounded text-base;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
@apply block font-bold mb-1;
|
||||||
|
@apply text-text-subtlest;
|
||||||
|
|
||||||
|
content: 'Note';
|
||||||
|
}
|
||||||
|
|
||||||
|
&.x-theme-banner--secondary::before {
|
||||||
|
content: 'Info';
|
||||||
|
}
|
||||||
|
|
||||||
|
&.x-theme-banner--success::before {
|
||||||
|
content: 'Tip';
|
||||||
|
}
|
||||||
|
|
||||||
|
&.x-theme-banner--notice::before {
|
||||||
|
content: 'Important';
|
||||||
|
}
|
||||||
|
|
||||||
|
&.x-theme-banner--warning::before {
|
||||||
|
content: 'Warning';
|
||||||
|
}
|
||||||
|
|
||||||
|
&.x-theme-banner--danger::before {
|
||||||
|
content: 'Caution';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
@apply italic py-3 pl-5 pr-3 border-l-8 border-surface-active text-lg text-text bg-surface-highlight rounded shadow-lg;
|
||||||
|
|
||||||
|
p {
|
||||||
|
@apply m-0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h2[id] > a .icon.icon-link {
|
||||||
|
@apply hidden w-4 h-4 bg-success ml-2;
|
||||||
|
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' stroke='currentColor' stroke-width='2' fill='none' stroke-linecap='round' stroke-linejoin='round' class='css-i6dzq1'%3E%3Cpath d='M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71'%3E%3C/path%3E%3Cpath d='M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71'%3E%3C/path%3E%3C/svg%3E");
|
||||||
|
mask-size: contain;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
@apply bg-notice;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h2[id]:hover {
|
||||||
|
.icon.icon-link {
|
||||||
|
@apply inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
@apply border-secondary border-dashed md:mx-[25%] my-10;
|
||||||
|
}
|
||||||
|
|
||||||
|
figure {
|
||||||
|
img {
|
||||||
|
@apply mb-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
figcaption {
|
||||||
|
@apply relative pl-9 text-success text-sm pt-1;
|
||||||
|
|
||||||
|
p {
|
||||||
|
@apply m-0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
figcaption::before {
|
||||||
|
@apply border-info absolute left-2 top-0 h-3.5 w-6 rounded-bl border-l border-b border-dotted;
|
||||||
|
content: '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import './Prose.css';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Prose({ className, ...props }: Props) {
|
||||||
|
return <div className={classNames('prose', className)} {...props} />;
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { HttpRequest, HttpRequestHeader, HttpUrlParameter } from '@yaakapp-internal/models';
|
import type { HttpRequest } from '@yaakapp-internal/models';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import type { CSSProperties } from 'react';
|
import type { CSSProperties } from 'react';
|
||||||
import React, { memo, useCallback, useMemo, useState } from 'react';
|
import React, { memo, useCallback, useMemo, useState } from 'react';
|
||||||
@@ -15,6 +15,7 @@ import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
|
|||||||
import { useSendAnyHttpRequest } from '../hooks/useSendAnyHttpRequest';
|
import { useSendAnyHttpRequest } from '../hooks/useSendAnyHttpRequest';
|
||||||
import { useUpdateAnyHttpRequest } from '../hooks/useUpdateAnyHttpRequest';
|
import { useUpdateAnyHttpRequest } from '../hooks/useUpdateAnyHttpRequest';
|
||||||
import { languageFromContentType } from '../lib/contentType';
|
import { languageFromContentType } from '../lib/contentType';
|
||||||
|
import { fallbackRequestName } from '../lib/fallbackRequestName';
|
||||||
import { tryFormatJson } from '../lib/formatters';
|
import { tryFormatJson } from '../lib/formatters';
|
||||||
import {
|
import {
|
||||||
AUTH_TYPE_BASIC,
|
AUTH_TYPE_BASIC,
|
||||||
@@ -34,9 +35,13 @@ import { BearerAuth } from './BearerAuth';
|
|||||||
import { BinaryFileEditor } from './BinaryFileEditor';
|
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 { GenericCompletionOption } from './core/Editor/genericCompletion';
|
import type {
|
||||||
|
GenericCompletionConfig,
|
||||||
|
GenericCompletionOption,
|
||||||
|
} from './core/Editor/genericCompletion';
|
||||||
import { InlineCode } from './core/InlineCode';
|
import { InlineCode } from './core/InlineCode';
|
||||||
import type { Pair } from './core/PairEditor';
|
import type { Pair } from './core/PairEditor';
|
||||||
|
import { PlainInput } from './core/PlainInput';
|
||||||
import type { TabItem } from './core/Tabs/Tabs';
|
import type { TabItem } from './core/Tabs/Tabs';
|
||||||
import { TabContent, Tabs } from './core/Tabs/Tabs';
|
import { TabContent, Tabs } from './core/Tabs/Tabs';
|
||||||
import { EmptyStateText } from './EmptyStateText';
|
import { EmptyStateText } from './EmptyStateText';
|
||||||
@@ -44,6 +49,7 @@ import { FormMultipartEditor } from './FormMultipartEditor';
|
|||||||
import { FormUrlencodedEditor } from './FormUrlencodedEditor';
|
import { FormUrlencodedEditor } from './FormUrlencodedEditor';
|
||||||
import { GraphQLEditor } from './GraphQLEditor';
|
import { GraphQLEditor } from './GraphQLEditor';
|
||||||
import { HeadersEditor } from './HeadersEditor';
|
import { HeadersEditor } from './HeadersEditor';
|
||||||
|
import { MarkdownEditor } from './MarkdownEditor';
|
||||||
import { useToast } from './ToastContext';
|
import { useToast } from './ToastContext';
|
||||||
import { UrlBar } from './UrlBar';
|
import { UrlBar } from './UrlBar';
|
||||||
import { UrlParametersEditor } from './UrlParameterEditor';
|
import { UrlParametersEditor } from './UrlParameterEditor';
|
||||||
@@ -59,8 +65,7 @@ const TAB_BODY = 'body';
|
|||||||
const TAB_PARAMS = 'params';
|
const TAB_PARAMS = 'params';
|
||||||
const TAB_HEADERS = 'headers';
|
const TAB_HEADERS = 'headers';
|
||||||
const TAB_AUTH = 'auth';
|
const TAB_AUTH = 'auth';
|
||||||
|
const TAB_DESCRIPTION = 'description';
|
||||||
const DEFAULT_TAB = TAB_BODY;
|
|
||||||
|
|
||||||
export const RequestPane = memo(function RequestPane({
|
export const RequestPane = memo(function RequestPane({
|
||||||
style,
|
style,
|
||||||
@@ -120,6 +125,15 @@ export const RequestPane = memo(function RequestPane({
|
|||||||
|
|
||||||
const tabs: TabItem[] = useMemo(
|
const tabs: TabItem[] = useMemo(
|
||||||
() => [
|
() => [
|
||||||
|
{
|
||||||
|
value: TAB_DESCRIPTION,
|
||||||
|
label: (
|
||||||
|
<div className="flex items-center">
|
||||||
|
Docs
|
||||||
|
{activeRequest.description && <CountBadge count={true} />}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
value: TAB_BODY,
|
value: TAB_BODY,
|
||||||
options: {
|
options: {
|
||||||
@@ -239,68 +253,36 @@ export const RequestPane = memo(function RequestPane({
|
|||||||
activeRequest.authentication,
|
activeRequest.authentication,
|
||||||
activeRequest.authenticationType,
|
activeRequest.authenticationType,
|
||||||
activeRequest.bodyType,
|
activeRequest.bodyType,
|
||||||
|
activeRequest.description,
|
||||||
activeRequest.headers,
|
activeRequest.headers,
|
||||||
activeRequest.method,
|
activeRequest.method,
|
||||||
activeRequestId,
|
activeRequestId,
|
||||||
handleContentTypeChange,
|
handleContentTypeChange,
|
||||||
toast,
|
toast,
|
||||||
updateRequest,
|
updateRequest,
|
||||||
urlParameterPairs,
|
urlParameterPairs.length,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const sendRequest = useSendAnyHttpRequest();
|
||||||
|
const { activeResponse } = usePinnedHttpResponse(activeRequest);
|
||||||
|
const cancelResponse = useCancelHttpResponse(activeResponse?.id ?? null);
|
||||||
|
const isLoading = useIsResponseLoading(activeRequestId);
|
||||||
|
const { updateKey } = useRequestUpdateKey(activeRequestId);
|
||||||
|
const importCurl = useImportCurl();
|
||||||
|
const importQuerystring = useImportQuerystring(activeRequestId);
|
||||||
|
|
||||||
const handleBodyChange = useCallback(
|
const handleBodyChange = useCallback(
|
||||||
(body: HttpRequest['body']) => updateRequest.mutate({ id: activeRequestId, update: { body } }),
|
(body: HttpRequest['body']) => updateRequest.mutate({ id: activeRequestId, update: { body } }),
|
||||||
[activeRequestId, updateRequest],
|
[activeRequestId, updateRequest],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleBinaryFileChange = useCallback(
|
|
||||||
(body: HttpRequest['body']) => {
|
|
||||||
updateRequest.mutate({ id: activeRequestId, update: { body } });
|
|
||||||
},
|
|
||||||
[activeRequestId, updateRequest],
|
|
||||||
);
|
|
||||||
const handleBodyTextChange = useCallback(
|
const handleBodyTextChange = useCallback(
|
||||||
(text: string) => updateRequest.mutate({ id: activeRequestId, update: { body: { text } } }),
|
(text: string) => updateRequest.mutate({ id: activeRequestId, update: { body: { text } } }),
|
||||||
[activeRequestId, updateRequest],
|
[activeRequestId, updateRequest],
|
||||||
);
|
);
|
||||||
const handleHeadersChange = useCallback(
|
|
||||||
(headers: HttpRequestHeader[]) =>
|
|
||||||
updateRequest.mutate({ id: activeRequestId, update: { headers } }),
|
|
||||||
[activeRequestId, updateRequest],
|
|
||||||
);
|
|
||||||
const handleUrlParametersChange = useCallback(
|
|
||||||
(urlParameters: HttpUrlParameter[]) =>
|
|
||||||
updateRequest.mutate({ id: activeRequestId, update: { urlParameters } }),
|
|
||||||
[activeRequestId, updateRequest],
|
|
||||||
);
|
|
||||||
|
|
||||||
const sendRequest = useSendAnyHttpRequest();
|
const activeTab = activeTabs?.[activeRequestId];
|
||||||
const { activeResponse } = usePinnedHttpResponse(activeRequest);
|
|
||||||
const cancelResponse = useCancelHttpResponse(activeResponse?.id ?? null);
|
|
||||||
const handleSend = useCallback(async () => {
|
|
||||||
await sendRequest.mutateAsync(activeRequest.id ?? null);
|
|
||||||
}, [activeRequest.id, sendRequest]);
|
|
||||||
|
|
||||||
const handleCancel = useCallback(async () => {
|
|
||||||
await cancelResponse.mutateAsync();
|
|
||||||
}, [cancelResponse]);
|
|
||||||
|
|
||||||
const handleMethodChange = useCallback(
|
|
||||||
(method: string) => updateRequest.mutate({ id: activeRequestId, update: { method } }),
|
|
||||||
[activeRequestId, updateRequest],
|
|
||||||
);
|
|
||||||
const handleUrlChange = useCallback(
|
|
||||||
(url: string) => updateRequest.mutate({ id: activeRequestId, update: { url } }),
|
|
||||||
[activeRequestId, updateRequest],
|
|
||||||
);
|
|
||||||
|
|
||||||
const isLoading = useIsResponseLoading(activeRequestId);
|
|
||||||
const { updateKey } = useRequestUpdateKey(activeRequestId);
|
|
||||||
const importCurl = useImportCurl();
|
|
||||||
const importQuerystring = useImportQuerystring(activeRequestId);
|
|
||||||
|
|
||||||
const activeTab = activeTabs?.[activeRequestId] ?? DEFAULT_TAB;
|
|
||||||
const setActiveTab = useCallback(
|
const setActiveTab = useCallback(
|
||||||
(tab: string) => {
|
(tab: string) => {
|
||||||
setActiveTabs((r) => ({ ...r, [activeRequest.id]: tab }));
|
setActiveTabs((r) => ({ ...r, [activeRequest.id]: tab }));
|
||||||
@@ -312,6 +294,21 @@ export const RequestPane = memo(function RequestPane({
|
|||||||
setActiveTab(TAB_PARAMS);
|
setActiveTab(TAB_PARAMS);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const autocomplete: GenericCompletionConfig = {
|
||||||
|
minMatch: 3,
|
||||||
|
options:
|
||||||
|
requests.length > 0
|
||||||
|
? [
|
||||||
|
...requests
|
||||||
|
.filter((r) => r.id !== activeRequestId)
|
||||||
|
.map((r): GenericCompletionOption => ({ type: 'constant', label: r.url })),
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
{ label: 'http://', type: 'constant' },
|
||||||
|
{ label: 'https://', type: 'constant' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={style}
|
style={style}
|
||||||
@@ -332,30 +329,15 @@ export const RequestPane = memo(function RequestPane({
|
|||||||
importQuerystring.mutate(text);
|
importQuerystring.mutate(text);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
autocomplete={{
|
autocomplete={autocomplete}
|
||||||
minMatch: 3,
|
onSend={() => sendRequest.mutateAsync(activeRequest.id ?? null)}
|
||||||
options:
|
onCancel={cancelResponse.mutate}
|
||||||
requests.length > 0
|
onMethodChange={(method) =>
|
||||||
? [
|
updateRequest.mutate({ id: activeRequestId, update: { method } })
|
||||||
...requests
|
}
|
||||||
.filter((r) => r.id !== activeRequestId)
|
onUrlChange={(url: string) =>
|
||||||
.map(
|
updateRequest.mutate({ id: activeRequestId, update: { url } })
|
||||||
(r) =>
|
}
|
||||||
({
|
|
||||||
type: 'constant',
|
|
||||||
label: r.url,
|
|
||||||
}) as GenericCompletionOption,
|
|
||||||
),
|
|
||||||
]
|
|
||||||
: [
|
|
||||||
{ label: 'http://', type: 'constant' },
|
|
||||||
{ label: 'https://', type: 'constant' },
|
|
||||||
],
|
|
||||||
}}
|
|
||||||
onSend={handleSend}
|
|
||||||
onCancel={handleCancel}
|
|
||||||
onMethodChange={handleMethodChange}
|
|
||||||
onUrlChange={handleUrlChange}
|
|
||||||
forceUpdateKey={updateKey}
|
forceUpdateKey={updateKey}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
/>
|
/>
|
||||||
@@ -382,14 +364,18 @@ export const RequestPane = memo(function RequestPane({
|
|||||||
<HeadersEditor
|
<HeadersEditor
|
||||||
forceUpdateKey={`${forceUpdateHeaderEditorKey}::${forceUpdateKey}`}
|
forceUpdateKey={`${forceUpdateHeaderEditorKey}::${forceUpdateKey}`}
|
||||||
headers={activeRequest.headers}
|
headers={activeRequest.headers}
|
||||||
onChange={handleHeadersChange}
|
onChange={(headers) =>
|
||||||
|
updateRequest.mutate({ id: activeRequestId, update: { headers } })
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</TabContent>
|
</TabContent>
|
||||||
<TabContent value={TAB_PARAMS}>
|
<TabContent value={TAB_PARAMS}>
|
||||||
<UrlParametersEditor
|
<UrlParametersEditor
|
||||||
forceUpdateKey={forceUpdateKey + urlParametersKey}
|
forceUpdateKey={forceUpdateKey + urlParametersKey}
|
||||||
pairs={urlParameterPairs}
|
pairs={urlParameterPairs}
|
||||||
onChange={handleUrlParametersChange}
|
onChange={(urlParameters) =>
|
||||||
|
updateRequest.mutate({ id: activeRequestId, update: { urlParameters } })
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</TabContent>
|
</TabContent>
|
||||||
<TabContent value={TAB_BODY}>
|
<TabContent value={TAB_BODY}>
|
||||||
@@ -440,7 +426,9 @@ export const RequestPane = memo(function RequestPane({
|
|||||||
requestId={activeRequest.id}
|
requestId={activeRequest.id}
|
||||||
contentType={contentType}
|
contentType={contentType}
|
||||||
body={activeRequest.body}
|
body={activeRequest.body}
|
||||||
onChange={handleBinaryFileChange}
|
onChange={(body) =>
|
||||||
|
updateRequest.mutate({ id: activeRequestId, update: { body } })
|
||||||
|
}
|
||||||
onChangeContentType={handleContentTypeChange}
|
onChangeContentType={handleContentTypeChange}
|
||||||
/>
|
/>
|
||||||
) : typeof activeRequest.bodyType === 'string' ? (
|
) : typeof activeRequest.bodyType === 'string' ? (
|
||||||
@@ -458,6 +446,26 @@ export const RequestPane = memo(function RequestPane({
|
|||||||
<EmptyStateText>Empty Body</EmptyStateText>
|
<EmptyStateText>Empty Body</EmptyStateText>
|
||||||
)}
|
)}
|
||||||
</TabContent>
|
</TabContent>
|
||||||
|
<TabContent value={TAB_DESCRIPTION}><div className="grid grid-rows-[auto_minmax(0,1fr)] h-full">
|
||||||
|
<PlainInput
|
||||||
|
label="Request Name"
|
||||||
|
hideLabel
|
||||||
|
defaultValue={activeRequest.name}
|
||||||
|
className="font-sans !text-xl !px-0"
|
||||||
|
containerClassName="border-0"
|
||||||
|
placeholder={fallbackRequestName(activeRequest)}
|
||||||
|
onChange={(name) => updateRequest.mutate({ id: activeRequestId, update: { name } })}
|
||||||
|
/>
|
||||||
|
<MarkdownEditor
|
||||||
|
name="request-description"
|
||||||
|
placeholder="A Markdown description of this request."
|
||||||
|
defaultValue={activeRequest.description}
|
||||||
|
onChange={(description) =>
|
||||||
|
updateRequest.mutate({ id: activeRequestId, update: { description } })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</TabContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ interface Props {
|
|||||||
const TAB_BODY = 'body';
|
const TAB_BODY = 'body';
|
||||||
const TAB_HEADERS = 'headers';
|
const TAB_HEADERS = 'headers';
|
||||||
const TAB_INFO = 'info';
|
const TAB_INFO = 'info';
|
||||||
const DEFAULT_TAB = TAB_BODY;
|
|
||||||
|
|
||||||
export const ResponsePane = memo(function ResponsePane({ style, className, activeRequest }: Props) {
|
export const ResponsePane = memo(function ResponsePane({ style, className, activeRequest }: Props) {
|
||||||
const { activeResponse, setPinnedResponseId, responses } = usePinnedHttpResponse(activeRequest);
|
const { activeResponse, setPinnedResponseId, responses } = usePinnedHttpResponse(activeRequest);
|
||||||
@@ -48,13 +47,6 @@ export const ResponsePane = memo(function ResponsePane({ style, className, activ
|
|||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
const contentType = useContentTypeFromHeaders(activeResponse?.headers ?? null);
|
const contentType = useContentTypeFromHeaders(activeResponse?.headers ?? null);
|
||||||
const activeTab = activeTabs?.[activeRequest.id] ?? DEFAULT_TAB;
|
|
||||||
const setActiveTab = useCallback(
|
|
||||||
(tab: string) => {
|
|
||||||
setActiveTabs((r) => ({ ...r, [activeRequest.id]: tab }));
|
|
||||||
},
|
|
||||||
[activeRequest.id, setActiveTabs],
|
|
||||||
);
|
|
||||||
|
|
||||||
const tabs = useMemo<TabItem[]>(
|
const tabs = useMemo<TabItem[]>(
|
||||||
() => [
|
() => [
|
||||||
@@ -88,6 +80,13 @@ export const ResponsePane = memo(function ResponsePane({ style, className, activ
|
|||||||
],
|
],
|
||||||
[activeResponse?.headers, contentType, setViewMode, viewMode],
|
[activeResponse?.headers, contentType, setViewMode, viewMode],
|
||||||
);
|
);
|
||||||
|
const activeTab = activeTabs?.[activeRequest.id];
|
||||||
|
const setActiveTab = useCallback(
|
||||||
|
(tab: string) => {
|
||||||
|
setActiveTabs((r) => ({ ...r, [activeRequest.id]: tab }));
|
||||||
|
},
|
||||||
|
[activeRequest.id, setActiveTabs],
|
||||||
|
);
|
||||||
|
|
||||||
const isLoading = isResponseLoading(activeResponse);
|
const isLoading = isResponseLoading(activeResponse);
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ import { useHttpRequestActions } from '../hooks/useHttpRequestActions';
|
|||||||
import { useHttpResponses } from '../hooks/useHttpResponses';
|
import { useHttpResponses } from '../hooks/useHttpResponses';
|
||||||
import { useKeyValue } from '../hooks/useKeyValue';
|
import { useKeyValue } from '../hooks/useKeyValue';
|
||||||
import { useMoveToWorkspace } from '../hooks/useMoveToWorkspace';
|
import { useMoveToWorkspace } from '../hooks/useMoveToWorkspace';
|
||||||
import { usePrompt } from '../hooks/usePrompt';
|
|
||||||
import { useRenameRequest } from '../hooks/useRenameRequest';
|
import { useRenameRequest } from '../hooks/useRenameRequest';
|
||||||
import { useRequests } from '../hooks/useRequests';
|
import { useRequests } from '../hooks/useRequests';
|
||||||
import { useScrollIntoView } from '../hooks/useScrollIntoView';
|
import { useScrollIntoView } from '../hooks/useScrollIntoView';
|
||||||
@@ -50,10 +49,11 @@ import type { DropdownItem } from './core/Dropdown';
|
|||||||
import { ContextMenu } from './core/Dropdown';
|
import { ContextMenu } from './core/Dropdown';
|
||||||
import { HttpMethodTag } from './core/HttpMethodTag';
|
import { HttpMethodTag } from './core/HttpMethodTag';
|
||||||
import { Icon } from './core/Icon';
|
import { Icon } from './core/Icon';
|
||||||
import { InlineCode } from './core/InlineCode';
|
|
||||||
import { VStack } from './core/Stacks';
|
import { VStack } from './core/Stacks';
|
||||||
import { StatusTag } from './core/StatusTag';
|
import { StatusTag } from './core/StatusTag';
|
||||||
|
import { useDialog } from './DialogContext';
|
||||||
import { DropMarker } from './DropMarker';
|
import { DropMarker } from './DropMarker';
|
||||||
|
import { FolderSettingsDialog } from './FolderSettingsDialog';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string;
|
className?: string;
|
||||||
@@ -694,6 +694,7 @@ function SidebarItem({
|
|||||||
|
|
||||||
connectDrag(connectDrop(ref));
|
connectDrag(connectDrop(ref));
|
||||||
|
|
||||||
|
const dialog = useDialog();
|
||||||
const activeRequest = useActiveRequest();
|
const activeRequest = useActiveRequest();
|
||||||
const deleteFolder = useDeleteFolder(itemId);
|
const deleteFolder = useDeleteFolder(itemId);
|
||||||
const deleteRequest = useDeleteRequest(itemId);
|
const deleteRequest = useDeleteRequest(itemId);
|
||||||
@@ -706,8 +707,6 @@ function SidebarItem({
|
|||||||
const updateHttpRequest = useUpdateAnyHttpRequest();
|
const updateHttpRequest = useUpdateAnyHttpRequest();
|
||||||
const workspaces = useWorkspaces();
|
const workspaces = useWorkspaces();
|
||||||
const updateGrpcRequest = useUpdateAnyGrpcRequest();
|
const updateGrpcRequest = useUpdateAnyGrpcRequest();
|
||||||
const updateAnyFolder = useUpdateAnyFolder();
|
|
||||||
const prompt = usePrompt();
|
|
||||||
const [editing, setEditing] = useState<boolean>(false);
|
const [editing, setEditing] = useState<boolean>(false);
|
||||||
const isActive = activeRequest?.id === itemId;
|
const isActive = activeRequest?.id === itemId;
|
||||||
const createDropdownItems = useCreateDropdownItems({ folderId: itemId });
|
const createDropdownItems = useCreateDropdownItems({ folderId: itemId });
|
||||||
@@ -786,35 +785,25 @@ function SidebarItem({
|
|||||||
if (itemModel === 'folder') {
|
if (itemModel === 'folder') {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
key: 'sendAll',
|
key: 'send-all',
|
||||||
label: 'Send All',
|
label: 'Send All',
|
||||||
leftSlot: <Icon icon="send_horizontal" />,
|
leftSlot: <Icon icon="send_horizontal" />,
|
||||||
onSelect: () => sendManyRequests.mutate(child.children.map((c) => c.item.id)),
|
onSelect: () => sendManyRequests.mutate(child.children.map((c) => c.item.id)),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'rename',
|
key: 'folder-settings',
|
||||||
label: 'Rename',
|
label: 'Settings',
|
||||||
leftSlot: <Icon icon="pencil" />,
|
leftSlot: <Icon icon="settings" />,
|
||||||
onSelect: async () => {
|
onSelect: () =>
|
||||||
const name = await prompt({
|
dialog.show({
|
||||||
id: 'rename-folder',
|
id: 'folder-settings',
|
||||||
title: 'Rename Folder',
|
title: 'Folder Settings',
|
||||||
description: (
|
size: 'md',
|
||||||
<>
|
render: () => <FolderSettingsDialog folderId={itemId} />,
|
||||||
Enter a new name for <InlineCode>{itemName}</InlineCode>
|
}),
|
||||||
</>
|
|
||||||
),
|
|
||||||
confirmText: 'Save',
|
|
||||||
label: 'Name',
|
|
||||||
placeholder: 'New Name',
|
|
||||||
defaultValue: itemName,
|
|
||||||
});
|
|
||||||
if (name == null) return;
|
|
||||||
updateAnyFolder.mutate({ id: itemId, update: (f) => ({ ...f, name }) });
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'deleteFolder',
|
key: 'delete-folder',
|
||||||
label: 'Delete',
|
label: 'Delete',
|
||||||
variant: 'danger',
|
variant: 'danger',
|
||||||
leftSlot: <Icon icon="trash" />,
|
leftSlot: <Icon icon="trash" />,
|
||||||
@@ -828,7 +817,7 @@ function SidebarItem({
|
|||||||
itemModel === 'http_request'
|
itemModel === 'http_request'
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
key: 'sendRequest',
|
key: 'send-request',
|
||||||
label: 'Send',
|
label: 'Send',
|
||||||
hotKeyAction: 'http_request.send',
|
hotKeyAction: 'http_request.send',
|
||||||
hotKeyLabelOnly: true, // Already bound in URL bar
|
hotKeyLabelOnly: true, // Already bound in URL bar
|
||||||
@@ -851,13 +840,13 @@ function SidebarItem({
|
|||||||
return [
|
return [
|
||||||
...requestItems,
|
...requestItems,
|
||||||
{
|
{
|
||||||
key: 'renameRequest',
|
key: 'rename-request',
|
||||||
label: 'Rename',
|
label: 'Rename',
|
||||||
leftSlot: <Icon icon="pencil" />,
|
leftSlot: <Icon icon="pencil" />,
|
||||||
onSelect: renameRequest.mutate,
|
onSelect: renameRequest.mutate,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'duplicateRequest',
|
key: 'duplicate-request',
|
||||||
label: 'Duplicate',
|
label: 'Duplicate',
|
||||||
hotKeyAction: 'http_request.duplicate',
|
hotKeyAction: 'http_request.duplicate',
|
||||||
hotKeyLabelOnly: true, // Would trigger for every request (bad)
|
hotKeyLabelOnly: true, // Would trigger for every request (bad)
|
||||||
@@ -868,14 +857,14 @@ function SidebarItem({
|
|||||||
: duplicateGrpcRequest.mutate(),
|
: duplicateGrpcRequest.mutate(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'moveWorkspace',
|
key: 'move-workspace',
|
||||||
label: 'Move',
|
label: 'Move',
|
||||||
leftSlot: <Icon icon="arrow_right_circle" />,
|
leftSlot: <Icon icon="arrow_right_circle" />,
|
||||||
hidden: workspaces.length <= 1,
|
hidden: workspaces.length <= 1,
|
||||||
onSelect: moveToWorkspace.mutate,
|
onSelect: moveToWorkspace.mutate,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'deleteRequest',
|
key: 'delete-request',
|
||||||
variant: 'danger',
|
variant: 'danger',
|
||||||
label: 'Delete',
|
label: 'Delete',
|
||||||
leftSlot: <Icon icon="trash" />,
|
leftSlot: <Icon icon="trash" />,
|
||||||
@@ -888,18 +877,16 @@ function SidebarItem({
|
|||||||
createDropdownItems,
|
createDropdownItems,
|
||||||
deleteFolder,
|
deleteFolder,
|
||||||
deleteRequest,
|
deleteRequest,
|
||||||
|
dialog,
|
||||||
duplicateGrpcRequest,
|
duplicateGrpcRequest,
|
||||||
duplicateHttpRequest,
|
duplicateHttpRequest,
|
||||||
httpRequestActions,
|
httpRequestActions,
|
||||||
itemId,
|
itemId,
|
||||||
itemModel,
|
itemModel,
|
||||||
itemName,
|
|
||||||
moveToWorkspace.mutate,
|
moveToWorkspace.mutate,
|
||||||
prompt,
|
|
||||||
renameRequest.mutate,
|
renameRequest.mutate,
|
||||||
sendManyRequests,
|
sendManyRequests,
|
||||||
sendRequest,
|
sendRequest,
|
||||||
updateAnyFolder,
|
|
||||||
workspaces.length,
|
workspaces.length,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export const UrlBar = memo(function UrlBar({
|
|||||||
<Input
|
<Input
|
||||||
autocompleteVariables
|
autocompleteVariables
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
size="sm"
|
size="md"
|
||||||
wrapLines={isFocused}
|
wrapLines={isFocused}
|
||||||
hideLabel
|
hideLabel
|
||||||
useTemplating
|
useTemplating
|
||||||
@@ -86,26 +86,30 @@ export const UrlBar = memo(function UrlBar({
|
|||||||
leftSlot={
|
leftSlot={
|
||||||
method != null &&
|
method != null &&
|
||||||
onMethodChange != null && (
|
onMethodChange != null && (
|
||||||
<RequestMethodDropdown
|
<div className="py-0.5">
|
||||||
method={method}
|
<RequestMethodDropdown
|
||||||
onChange={onMethodChange}
|
method={method}
|
||||||
className="my-0.5 ml-0.5"
|
onChange={onMethodChange}
|
||||||
/>
|
className="ml-0.5 !h-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
rightSlot={
|
rightSlot={
|
||||||
<>
|
<>
|
||||||
{rightSlot}
|
{rightSlot}
|
||||||
{submitIcon !== null && (
|
{submitIcon !== null && (
|
||||||
<IconButton
|
<div className="py-0.5">
|
||||||
size="xs"
|
<IconButton
|
||||||
iconSize="md"
|
size="xs"
|
||||||
title="Send Request"
|
iconSize="md"
|
||||||
type="submit"
|
title="Send Request"
|
||||||
className="w-8 my-0.5 mr-0.5"
|
type="submit"
|
||||||
icon={isLoading ? 'x' : submitIcon}
|
className="w-8 mr-0.5 !h-full"
|
||||||
hotkeyAction="http_request.send"
|
icon={isLoading ? 'x' : submitIcon}
|
||||||
/>
|
hotkeyAction="http_request.send"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { useUpdateWorkspace } from '../hooks/useUpdateWorkspace';
|
||||||
|
import { useWorkspaces } from '../hooks/useWorkspaces';
|
||||||
|
import { Banner } from './core/Banner';
|
||||||
|
import { PlainInput } from './core/PlainInput';
|
||||||
|
import { VStack } from './core/Stacks';
|
||||||
|
import { MarkdownEditor } from './MarkdownEditor';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
workspaceId: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WorkspaceSettingsDialog({ workspaceId }: Props) {
|
||||||
|
const updateWorkspace = useUpdateWorkspace(workspaceId ?? null);
|
||||||
|
const workspaces = useWorkspaces();
|
||||||
|
const workspace = workspaces.find((w) => w.id === workspaceId);
|
||||||
|
|
||||||
|
if (workspace == null) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VStack space={3} className="pb-3">
|
||||||
|
{updateWorkspace.error != null && (
|
||||||
|
<Banner color="danger">{String(updateWorkspace.error)}</Banner>
|
||||||
|
)}
|
||||||
|
<PlainInput
|
||||||
|
label="Workspace Name"
|
||||||
|
defaultValue={workspace.name}
|
||||||
|
onChange={(name) => updateWorkspace.mutate({ name })}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<MarkdownEditor
|
||||||
|
name="workspace-description"
|
||||||
|
placeholder="A Markdown description of this workspace."
|
||||||
|
className="min-h-[10rem] border border-border px-2"
|
||||||
|
defaultValue={workspace.description}
|
||||||
|
onChange={(description) => updateWorkspace.mutate({ description })}
|
||||||
|
/>
|
||||||
|
</VStack>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,7 +2,6 @@ import classNames from 'classnames';
|
|||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import type { CSSProperties, MouseEvent as ReactMouseEvent } from 'react';
|
import type { CSSProperties, MouseEvent as ReactMouseEvent } from 'react';
|
||||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||||
import { useWindowSize } from 'react-use';
|
|
||||||
import { useActiveRequest } from '../hooks/useActiveRequest';
|
import { useActiveRequest } from '../hooks/useActiveRequest';
|
||||||
import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
|
import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
|
||||||
import { useFloatingSidebarHidden } from '../hooks/useFloatingSidebarHidden';
|
import { useFloatingSidebarHidden } from '../hooks/useFloatingSidebarHidden';
|
||||||
@@ -34,13 +33,13 @@ const drag = { gridArea: 'drag' };
|
|||||||
|
|
||||||
export default function Workspace() {
|
export default function Workspace() {
|
||||||
useSyncWorkspaceRequestTitle();
|
useSyncWorkspaceRequestTitle();
|
||||||
|
|
||||||
const workspaces = useWorkspaces();
|
const workspaces = useWorkspaces();
|
||||||
const activeWorkspace = useActiveWorkspace();
|
const activeWorkspace = useActiveWorkspace();
|
||||||
const { setWidth, width, resetWidth } = useSidebarWidth();
|
const { setWidth, width, resetWidth } = useSidebarWidth();
|
||||||
const [sidebarHidden, setSidebarHidden] = useSidebarHidden();
|
const [sidebarHidden, setSidebarHidden] = useSidebarHidden();
|
||||||
const [floatingSidebarHidden, setFloatingSidebarHidden] = useFloatingSidebarHidden();
|
const [floatingSidebarHidden, setFloatingSidebarHidden] = useFloatingSidebarHidden();
|
||||||
const activeRequest = useActiveRequest();
|
const activeRequest = useActiveRequest();
|
||||||
const windowSize = useWindowSize();
|
|
||||||
const importData = useImportData();
|
const importData = useImportData();
|
||||||
const floating = useShouldFloatSidebar();
|
const floating = useShouldFloatSidebar();
|
||||||
const [isResizing, setIsResizing] = useState<boolean>(false);
|
const [isResizing, setIsResizing] = useState<boolean>(false);
|
||||||
@@ -103,14 +102,6 @@ export default function Workspace() {
|
|||||||
[sideWidth, floating],
|
[sideWidth, floating],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (windowSize.width <= 100) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Button>Send</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// We're loading still
|
// We're loading still
|
||||||
if (workspaces.length === 0) {
|
if (workspaces.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -5,20 +5,18 @@ import { useCreateWorkspace } from '../hooks/useCreateWorkspace';
|
|||||||
import { useDeleteSendHistory } from '../hooks/useDeleteSendHistory';
|
import { useDeleteSendHistory } from '../hooks/useDeleteSendHistory';
|
||||||
import { useDeleteWorkspace } from '../hooks/useDeleteWorkspace';
|
import { useDeleteWorkspace } from '../hooks/useDeleteWorkspace';
|
||||||
import { useOpenWorkspace } from '../hooks/useOpenWorkspace';
|
import { useOpenWorkspace } from '../hooks/useOpenWorkspace';
|
||||||
import { usePrompt } from '../hooks/usePrompt';
|
|
||||||
import { useSettings } from '../hooks/useSettings';
|
import { useSettings } from '../hooks/useSettings';
|
||||||
import { useUpdateWorkspace } from '../hooks/useUpdateWorkspace';
|
|
||||||
import { useWorkspaces } from '../hooks/useWorkspaces';
|
import { useWorkspaces } from '../hooks/useWorkspaces';
|
||||||
import { getWorkspace } from '../lib/store';
|
import { getWorkspace } from '../lib/store';
|
||||||
import type { ButtonProps } from './core/Button';
|
import type { ButtonProps } from './core/Button';
|
||||||
import { Button } from './core/Button';
|
import { Button } from './core/Button';
|
||||||
import type { DropdownItem } from './core/Dropdown';
|
import type { DropdownItem } from './core/Dropdown';
|
||||||
import { Icon } from './core/Icon';
|
import { Icon } from './core/Icon';
|
||||||
import { InlineCode } from './core/InlineCode';
|
|
||||||
import type { RadioDropdownItem } from './core/RadioDropdown';
|
import type { RadioDropdownItem } from './core/RadioDropdown';
|
||||||
import { RadioDropdown } from './core/RadioDropdown';
|
import { RadioDropdown } from './core/RadioDropdown';
|
||||||
import { useDialog } from './DialogContext';
|
import { useDialog } from './DialogContext';
|
||||||
import { OpenWorkspaceDialog } from './OpenWorkspaceDialog';
|
import { OpenWorkspaceDialog } from './OpenWorkspaceDialog';
|
||||||
|
import { WorkspaceSettingsDialog } from './WorkpaceSettingsDialog';
|
||||||
|
|
||||||
type Props = Pick<ButtonProps, 'className' | 'justify' | 'forDropdown' | 'leftSlot'>;
|
type Props = Pick<ButtonProps, 'className' | 'justify' | 'forDropdown' | 'leftSlot'>;
|
||||||
|
|
||||||
@@ -29,11 +27,9 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
|
|||||||
const workspaces = useWorkspaces();
|
const workspaces = useWorkspaces();
|
||||||
const activeWorkspace = useActiveWorkspace();
|
const activeWorkspace = useActiveWorkspace();
|
||||||
const activeWorkspaceId = activeWorkspace?.id ?? null;
|
const activeWorkspaceId = activeWorkspace?.id ?? null;
|
||||||
const updateWorkspace = useUpdateWorkspace(activeWorkspaceId);
|
|
||||||
const deleteWorkspace = useDeleteWorkspace(activeWorkspace);
|
const deleteWorkspace = useDeleteWorkspace(activeWorkspace);
|
||||||
const createWorkspace = useCreateWorkspace();
|
const createWorkspace = useCreateWorkspace();
|
||||||
const dialog = useDialog();
|
const dialog = useDialog();
|
||||||
const prompt = usePrompt();
|
|
||||||
const settings = useSettings();
|
const settings = useSettings();
|
||||||
const openWorkspace = useOpenWorkspace();
|
const openWorkspace = useOpenWorkspace();
|
||||||
const openWorkspaceNewWindow = settings?.openWorkspaceNewWindow ?? null;
|
const openWorkspaceNewWindow = settings?.openWorkspaceNewWindow ?? null;
|
||||||
@@ -52,24 +48,16 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
|
|||||||
|
|
||||||
const extraItems: DropdownItem[] = [
|
const extraItems: DropdownItem[] = [
|
||||||
{
|
{
|
||||||
key: 'rename',
|
key: 'workspace-settings',
|
||||||
label: 'Rename',
|
label: 'Settings',
|
||||||
leftSlot: <Icon icon="pencil" />,
|
leftSlot: <Icon icon="settings" />,
|
||||||
onSelect: async () => {
|
onSelect: async () => {
|
||||||
const name = await prompt({
|
dialog.show({
|
||||||
id: 'rename-workspace',
|
id: 'workspace-settings',
|
||||||
title: 'Rename Workspace',
|
title: 'Workspace Settings',
|
||||||
description: (
|
size: 'md',
|
||||||
<>
|
render: () => <WorkspaceSettingsDialog workspaceId={activeWorkspace?.id ?? null} />,
|
||||||
Enter a new name for <InlineCode>{activeWorkspace?.name}</InlineCode>
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
label: 'Name',
|
|
||||||
placeholder: 'New Name',
|
|
||||||
defaultValue: activeWorkspace?.name,
|
|
||||||
});
|
});
|
||||||
if (name == null) return;
|
|
||||||
updateWorkspace.mutate({ name });
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -96,13 +84,12 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
|
|||||||
|
|
||||||
return { workspaceItems, extraItems };
|
return { workspaceItems, extraItems };
|
||||||
}, [
|
}, [
|
||||||
activeWorkspace?.name,
|
activeWorkspace,
|
||||||
activeWorkspaceId,
|
activeWorkspaceId,
|
||||||
createWorkspace.mutate,
|
createWorkspace.mutate,
|
||||||
deleteSendHistory.mutate,
|
deleteSendHistory.mutate,
|
||||||
deleteWorkspace.mutate,
|
deleteWorkspace.mutate,
|
||||||
prompt,
|
dialog,
|
||||||
updateWorkspace,
|
|
||||||
workspaces,
|
workspaces,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ export const WorkspaceHeader = memo(function WorkspaceHeader({ className }: Prop
|
|||||||
icon="search"
|
icon="search"
|
||||||
title="Search or execute a command"
|
title="Search or execute a command"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
event="search"
|
||||||
onClick={togglePalette}
|
onClick={togglePalette}
|
||||||
/>
|
/>
|
||||||
<SettingsDropdown />
|
<SettingsDropdown />
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
count: number;
|
count: number | true;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -12,10 +12,11 @@ export function CountBadge({ count, className }: Props) {
|
|||||||
aria-hidden
|
aria-hidden
|
||||||
className={classNames(
|
className={classNames(
|
||||||
className,
|
className,
|
||||||
|
'flex items-center',
|
||||||
'opacity-70 border border-border-subtle text-4xs rounded mb-0.5 px-1 ml-1 h-4 font-mono',
|
'opacity-70 border border-border-subtle text-4xs rounded mb-0.5 px-1 ml-1 h-4 font-mono',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{count}
|
{count === true ? <div aria-hidden className="rounded-full h-1 w-1 bg-text-subtle" /> : count}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
.cm-line {
|
.cm-line {
|
||||||
@apply w-full;
|
@apply w-full;
|
||||||
/* Important! Ensure it spans the entire width */
|
/* Important! Ensure it spans the entire width */
|
||||||
@apply w-full text-text pl-1 pr-1.5;
|
@apply w-full text-text px-0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cm-placeholder {
|
.cm-placeholder {
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
/* Style gutters */
|
/* Style gutters */
|
||||||
|
|
||||||
.cm-gutters {
|
.cm-gutters {
|
||||||
@apply border-0 text-text-subtlest bg-surface;
|
@apply border-0 text-text-subtlest bg-surface pr-1.5;
|
||||||
/* Not sure why, but there's a tiny gap left of the gutter that you can see text
|
/* Not sure why, but there's a tiny gap left of the gutter that you can see text
|
||||||
through. Move left slightly to fix that. */
|
through. Move left slightly to fix that. */
|
||||||
@apply -left-[1px];
|
@apply -left-[1px];
|
||||||
@@ -114,12 +114,6 @@
|
|||||||
|
|
||||||
.cm-scroller {
|
.cm-scroller {
|
||||||
@apply font-mono text-editor;
|
@apply font-mono text-editor;
|
||||||
/*
|
|
||||||
* Round corners or they'll stick out of the editor bounds of editor is rounded.
|
|
||||||
* Could potentially be pushed up from the editor like we do with bg color but this
|
|
||||||
* is probably fine.
|
|
||||||
*/
|
|
||||||
@apply rounded-lg;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ import classNames from 'classnames';
|
|||||||
import { EditorView } from 'codemirror';
|
import { EditorView } from 'codemirror';
|
||||||
import type { MutableRefObject, ReactNode } from 'react';
|
import type { MutableRefObject, ReactNode } from 'react';
|
||||||
import {
|
import {
|
||||||
useEffect,
|
|
||||||
Children,
|
Children,
|
||||||
cloneElement,
|
cloneElement,
|
||||||
forwardRef,
|
forwardRef,
|
||||||
isValidElement,
|
isValidElement,
|
||||||
useCallback,
|
useCallback,
|
||||||
|
useEffect,
|
||||||
useImperativeHandle,
|
useImperativeHandle,
|
||||||
useMemo,
|
useMemo,
|
||||||
useRef,
|
useRef,
|
||||||
@@ -45,7 +45,16 @@ export interface EditorProps {
|
|||||||
type?: 'text' | 'password';
|
type?: 'text' | 'password';
|
||||||
className?: string;
|
className?: string;
|
||||||
heightMode?: 'auto' | 'full';
|
heightMode?: 'auto' | 'full';
|
||||||
language?: 'javascript' | 'json' | 'html' | 'xml' | 'graphql' | 'url' | 'pairs' | 'text';
|
language?:
|
||||||
|
| 'javascript'
|
||||||
|
| 'json'
|
||||||
|
| 'html'
|
||||||
|
| 'xml'
|
||||||
|
| 'graphql'
|
||||||
|
| 'url'
|
||||||
|
| 'pairs'
|
||||||
|
| 'text'
|
||||||
|
| 'markdown';
|
||||||
forceUpdateKey?: string | number;
|
forceUpdateKey?: string | number;
|
||||||
autoFocus?: boolean;
|
autoFocus?: boolean;
|
||||||
autoSelect?: boolean;
|
autoSelect?: boolean;
|
||||||
@@ -66,6 +75,7 @@ export interface EditorProps {
|
|||||||
autocompleteVariables?: boolean;
|
autocompleteVariables?: boolean;
|
||||||
extraExtensions?: Extension[];
|
extraExtensions?: Extension[];
|
||||||
actions?: ReactNode;
|
actions?: ReactNode;
|
||||||
|
hideGutter?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const emptyVariables: EnvironmentVariable[] = [];
|
const emptyVariables: EnvironmentVariable[] = [];
|
||||||
@@ -96,6 +106,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
|
|||||||
autocompleteVariables,
|
autocompleteVariables,
|
||||||
actions,
|
actions,
|
||||||
wrapLines,
|
wrapLines,
|
||||||
|
hideGutter,
|
||||||
}: EditorProps,
|
}: EditorProps,
|
||||||
ref,
|
ref,
|
||||||
) {
|
) {
|
||||||
@@ -310,6 +321,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
|
|||||||
container,
|
container,
|
||||||
readOnly,
|
readOnly,
|
||||||
singleLine,
|
singleLine,
|
||||||
|
hideGutter,
|
||||||
onChange: handleChange,
|
onChange: handleChange,
|
||||||
onPaste: handlePaste,
|
onPaste: handlePaste,
|
||||||
onPasteOverwrite: handlePasteOverwrite,
|
onPasteOverwrite: handlePasteOverwrite,
|
||||||
@@ -374,7 +386,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
|
|||||||
const decoratedActions = useMemo(() => {
|
const decoratedActions = useMemo(() => {
|
||||||
const results = [];
|
const results = [];
|
||||||
const actionClassName = classNames(
|
const actionClassName = classNames(
|
||||||
'bg-surface transition-opacity opacity-0 group-hover:opacity-100 hover:!opacity-100 shadow',
|
'bg-surface transition-opacity transform-gpu opacity-0 group-hover:opacity-100 hover:!opacity-100 shadow',
|
||||||
);
|
);
|
||||||
|
|
||||||
if (format) {
|
if (format) {
|
||||||
@@ -455,13 +467,14 @@ function getExtensions({
|
|||||||
container,
|
container,
|
||||||
readOnly,
|
readOnly,
|
||||||
singleLine,
|
singleLine,
|
||||||
|
hideGutter,
|
||||||
onChange,
|
onChange,
|
||||||
onPaste,
|
onPaste,
|
||||||
onPasteOverwrite,
|
onPasteOverwrite,
|
||||||
onFocus,
|
onFocus,
|
||||||
onBlur,
|
onBlur,
|
||||||
onKeyDown,
|
onKeyDown,
|
||||||
}: Pick<EditorProps, 'singleLine' | 'readOnly'> & {
|
}: Pick<EditorProps, 'singleLine' | 'readOnly' | 'hideGutter'> & {
|
||||||
container: HTMLDivElement | null;
|
container: HTMLDivElement | null;
|
||||||
onChange: MutableRefObject<EditorProps['onChange']>;
|
onChange: MutableRefObject<EditorProps['onChange']>;
|
||||||
onPaste: MutableRefObject<EditorProps['onPaste']>;
|
onPaste: MutableRefObject<EditorProps['onPaste']>;
|
||||||
@@ -499,7 +512,7 @@ function getExtensions({
|
|||||||
tooltips({ parent }),
|
tooltips({ parent }),
|
||||||
keymap.of(singleLine ? defaultKeymap.filter((k) => k.key !== 'Enter') : defaultKeymap),
|
keymap.of(singleLine ? defaultKeymap.filter((k) => k.key !== 'Enter') : defaultKeymap),
|
||||||
...(singleLine ? [singleLineExt()] : []),
|
...(singleLine ? [singleLineExt()] : []),
|
||||||
...(!singleLine ? [multiLineExtensions] : []),
|
...(!singleLine ? [multiLineExtensions({ hideGutter })] : []),
|
||||||
...(readOnly
|
...(readOnly
|
||||||
? [EditorState.readOnly.of(true), EditorView.contentAttributes.of({ tabindex: '-1' })]
|
? [EditorState.readOnly.of(true), EditorView.contentAttributes.of({ tabindex: '-1' })]
|
||||||
: []),
|
: []),
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
} from '@codemirror/autocomplete';
|
} from '@codemirror/autocomplete';
|
||||||
import { history, historyKeymap, indentWithTab } from '@codemirror/commands';
|
import { history, historyKeymap, indentWithTab } from '@codemirror/commands';
|
||||||
import { javascript } from '@codemirror/lang-javascript';
|
import { javascript } from '@codemirror/lang-javascript';
|
||||||
|
import { markdown } from '@codemirror/lang-markdown';
|
||||||
import { json } from '@codemirror/lang-json';
|
import { json } from '@codemirror/lang-json';
|
||||||
import { xml } from '@codemirror/lang-xml';
|
import { xml } from '@codemirror/lang-xml';
|
||||||
import type { LanguageSupport } from '@codemirror/language';
|
import type { LanguageSupport } from '@codemirror/language';
|
||||||
@@ -79,6 +80,7 @@ const syntaxExtensions: Record<NonNullable<EditorProps['language']>, LanguageSup
|
|||||||
url: url(),
|
url: url(),
|
||||||
pairs: pairs(),
|
pairs: pairs(),
|
||||||
text: text(),
|
text: text(),
|
||||||
|
markdown: markdown(),
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getLanguageExtension({
|
export function getLanguageExtension({
|
||||||
@@ -138,21 +140,25 @@ export const baseExtensions = [
|
|||||||
keymap.of([...historyKeymap, ...completionKeymap]),
|
keymap.of([...historyKeymap, ...completionKeymap]),
|
||||||
];
|
];
|
||||||
|
|
||||||
export const multiLineExtensions = [
|
export const multiLineExtensions = ({ hideGutter }: { hideGutter?: boolean }) => [
|
||||||
lineNumbers(),
|
hideGutter
|
||||||
foldGutter({
|
? []
|
||||||
markerDOM: (open) => {
|
: [
|
||||||
const el = document.createElement('div');
|
lineNumbers(),
|
||||||
el.classList.add('fold-gutter-icon');
|
foldGutter({
|
||||||
el.tabIndex = -1;
|
markerDOM: (open) => {
|
||||||
if (open) {
|
const el = document.createElement('div');
|
||||||
el.setAttribute('data-open', '');
|
el.classList.add('fold-gutter-icon');
|
||||||
}
|
el.tabIndex = -1;
|
||||||
return el;
|
if (open) {
|
||||||
},
|
el.setAttribute('data-open', '');
|
||||||
}),
|
}
|
||||||
|
return el;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
codeFolding({
|
codeFolding({
|
||||||
placeholderDOM(view, onclick, prepared) {
|
placeholderDOM(_view, onclick, prepared) {
|
||||||
const el = document.createElement('span');
|
const el = document.createElement('span');
|
||||||
el.onclick = onclick;
|
el.onclick = onclick;
|
||||||
el.className = 'cm-foldPlaceholder';
|
el.className = 'cm-foldPlaceholder';
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ const icons = {
|
|||||||
circle_alert: lucide.CircleAlertIcon,
|
circle_alert: lucide.CircleAlertIcon,
|
||||||
clock: lucide.ClockIcon,
|
clock: lucide.ClockIcon,
|
||||||
code: lucide.CodeIcon,
|
code: lucide.CodeIcon,
|
||||||
|
columns_2: lucide.Columns2Icon,
|
||||||
cookie: lucide.CookieIcon,
|
cookie: lucide.CookieIcon,
|
||||||
copy: lucide.CopyIcon,
|
copy: lucide.CopyIcon,
|
||||||
copy_check: lucide.CopyCheck,
|
copy_check: lucide.CopyCheck,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import useResizeObserver from '@react-hook/resize-observer';
|
import useSize from '@react-hook/size';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import type { CSSProperties, MouseEvent as ReactMouseEvent, ReactNode } from 'react';
|
import type { CSSProperties, MouseEvent as ReactMouseEvent, ReactNode } from 'react';
|
||||||
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
||||||
@@ -43,7 +43,6 @@ export function SplitLayout({
|
|||||||
}: Props) {
|
}: Props) {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const activeWorkspace = useActiveWorkspace();
|
const activeWorkspace = useActiveWorkspace();
|
||||||
const [verticalBasedOnSize, setVerticalBasedOnSize] = useState<boolean>(false);
|
|
||||||
const [widthRaw, setWidth] = useLocalStorage<number>(
|
const [widthRaw, setWidth] = useLocalStorage<number>(
|
||||||
`${name}_width::${activeWorkspace?.id ?? 'n/a'}`,
|
`${name}_width::${activeWorkspace?.id ?? 'n/a'}`,
|
||||||
);
|
);
|
||||||
@@ -62,10 +61,7 @@ export function SplitLayout({
|
|||||||
minHeightPx = 0;
|
minHeightPx = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
useResizeObserver(containerRef.current, ({ contentRect }) => {
|
const verticalBasedOnSize = useSize(containerRef.current)[0] < STACK_VERTICAL_WIDTH;
|
||||||
setVerticalBasedOnSize(contentRect.width < STACK_VERTICAL_WIDTH);
|
|
||||||
});
|
|
||||||
|
|
||||||
const vertical = layout !== 'horizontal' && (layout === 'vertical' || verticalBasedOnSize);
|
const vertical = layout !== 'horizontal' && (layout === 'vertical' || verticalBasedOnSize);
|
||||||
|
|
||||||
const styles = useMemo<CSSProperties>(() => {
|
const styles = useMemo<CSSProperties>(() => {
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ export function Tabs({
|
|||||||
}: Props) {
|
}: Props) {
|
||||||
const ref = useRef<HTMLDivElement | null>(null);
|
const ref = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
value = value ?? tabs[0]?.value;
|
||||||
|
|
||||||
// Update tabs when value changes
|
// Update tabs when value changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const tabs = ref.current?.querySelectorAll<HTMLDivElement>(`[data-tab]`);
|
const tabs = ref.current?.querySelectorAll<HTMLDivElement>(`[data-tab]`);
|
||||||
@@ -61,7 +63,7 @@ export function Tabs({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={classNames(className, 'h-full grid grid-rows-[auto_minmax(0,1fr)] grid-cols-1')}
|
className={classNames(className, 'h-full grid grid-rows-[auto_minmax(0,1fr)] grid-cols-1 overflow-x-hidden')}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
aria-label={label}
|
aria-label={label}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import useResizeObserver from '@react-hook/resize-observer';
|
|
||||||
import 'react-pdf/dist/Page/TextLayer.css';
|
import 'react-pdf/dist/Page/TextLayer.css';
|
||||||
import 'react-pdf/dist/Page/AnnotationLayer.css';
|
import 'react-pdf/dist/Page/AnnotationLayer.css';
|
||||||
import { convertFileSrc } from '@tauri-apps/api/core';
|
import { convertFileSrc } from '@tauri-apps/api/core';
|
||||||
@@ -6,7 +5,7 @@ import './PdfViewer.css';
|
|||||||
import type { PDFDocumentProxy } from 'pdfjs-dist';
|
import type { PDFDocumentProxy } from 'pdfjs-dist';
|
||||||
import React, { useRef, useState } from 'react';
|
import React, { useRef, useState } from 'react';
|
||||||
import { Document, Page } from 'react-pdf';
|
import { Document, Page } from 'react-pdf';
|
||||||
import { useDebouncedState } from '../../hooks/useDebouncedState';
|
import useSize from '@react-hook/size';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
bodyPath: string;
|
bodyPath: string;
|
||||||
@@ -19,12 +18,9 @@ const options = {
|
|||||||
|
|
||||||
export function PdfViewer({ bodyPath }: Props) {
|
export function PdfViewer({ bodyPath }: Props) {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const [containerWidth, setContainerWidth] = useDebouncedState<number>(0, 100);
|
|
||||||
const [numPages, setNumPages] = useState<number>();
|
const [numPages, setNumPages] = useState<number>();
|
||||||
|
|
||||||
useResizeObserver(containerRef.current ?? null, (v) => {
|
const [containerWidth] = useSize(containerRef.current);
|
||||||
setContainerWidth(v.contentRect.width);
|
|
||||||
});
|
|
||||||
|
|
||||||
const onDocumentLoadSuccess = ({ numPages: nextNumPages }: PDFDocumentProxy): void => {
|
const onDocumentLoadSuccess = ({ numPages: nextNumPages }: PDFDocumentProxy): void => {
|
||||||
setNumPages(nextNumPages);
|
setNumPages(nextNumPages);
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ function themeVariables(theme?: Partial<YaakColors>, base?: CSSVariables): CSSVa
|
|||||||
theme?.shadow ??
|
theme?.shadow ??
|
||||||
YaakColor.black().translucify(isThemeDark(theme ?? ({} as Partial<YaakColors>)) ? 0.7 : 0.93),
|
YaakColor.black().translucify(isThemeDark(theme ?? ({} as Partial<YaakColors>)) ? 0.7 : 0.93),
|
||||||
primary: theme?.primary,
|
primary: theme?.primary,
|
||||||
secondary: theme?.primary,
|
secondary: theme?.secondary,
|
||||||
info: theme?.info,
|
info: theme?.info,
|
||||||
success: theme?.success,
|
success: theme?.success,
|
||||||
notice: theme?.notice,
|
notice: theme?.notice,
|
||||||
|
|||||||
@@ -12,12 +12,13 @@
|
|||||||
"@codemirror/commands": "6.7.0",
|
"@codemirror/commands": "6.7.0",
|
||||||
"@codemirror/lang-javascript": "^6.2.2",
|
"@codemirror/lang-javascript": "^6.2.2",
|
||||||
"@codemirror/lang-json": "^6.0.1",
|
"@codemirror/lang-json": "^6.0.1",
|
||||||
|
"@codemirror/lang-markdown": "^6.3.1",
|
||||||
"@codemirror/lang-xml": "^6.0.2",
|
"@codemirror/lang-xml": "^6.0.2",
|
||||||
"@codemirror/language": "^6.6.0",
|
"@codemirror/language": "^6.6.0",
|
||||||
"@codemirror/search": "^6.2.3",
|
"@codemirror/search": "^6.2.3",
|
||||||
"@lezer/highlight": "^1.1.3",
|
"@lezer/highlight": "^1.1.3",
|
||||||
"@lezer/lr": "^1.3.3",
|
"@lezer/lr": "^1.3.3",
|
||||||
"@react-hook/resize-observer": "^2.0.2",
|
"@react-hook/size": "^2.1.2",
|
||||||
"@tailwindcss/container-queries": "^0.1.1",
|
"@tailwindcss/container-queries": "^0.1.1",
|
||||||
"@tanstack/react-query": "^5.59.16",
|
"@tanstack/react-query": "^5.59.16",
|
||||||
"@tanstack/react-virtual": "^3.10.8",
|
"@tanstack/react-virtual": "^3.10.8",
|
||||||
@@ -49,9 +50,11 @@
|
|||||||
"react-dnd-html5-backend": "^16.0.1",
|
"react-dnd-html5-backend": "^16.0.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-helmet-async": "^2.0.5",
|
"react-helmet-async": "^2.0.5",
|
||||||
|
"react-markdown": "^9.0.1",
|
||||||
"react-pdf": "^9.1.0",
|
"react-pdf": "^9.1.0",
|
||||||
"react-router-dom": "^6.26.2",
|
"react-router-dom": "^6.26.2",
|
||||||
"react-use": "^17.5.1",
|
"react-use": "^17.5.1",
|
||||||
|
"remark-gfm": "^4.0.0",
|
||||||
"slugify": "^1.6.6",
|
"slugify": "^1.6.6",
|
||||||
"uuid": "^10.0.0",
|
"uuid": "^10.0.0",
|
||||||
"whatwg-mimetype": "^4.0.0",
|
"whatwg-mimetype": "^4.0.0",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ const sizes = {
|
|||||||
'2xs': '1.4rem',
|
'2xs': '1.4rem',
|
||||||
xs: '1.8rem',
|
xs: '1.8rem',
|
||||||
sm: '2.0rem',
|
sm: '2.0rem',
|
||||||
md: '2.5rem',
|
md: '2.3rem',
|
||||||
};
|
};
|
||||||
|
|
||||||
/** @type {import("tailwindcss").Config} */
|
/** @type {import("tailwindcss").Config} */
|
||||||
|
|||||||
Reference in New Issue
Block a user