Markdown documentation for HTTP requests (#145)

This commit is contained in:
Gregory Schier
2024-12-19 05:57:40 -08:00
committed by GitHub
parent 42d350ef27
commit 833dc7d3f7
30 changed files with 2274 additions and 251 deletions
+1548 -12
View File
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;
+3 -3
View File
@@ -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]
+3 -3
View File
@@ -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, };
+9
View File
@@ -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")?,
+18 -9
View File
@@ -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>
);
}
+51 -14
View File
@@ -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>
); );
+137
View File
@@ -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>
);
}
+201
View File
@@ -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: '';
}
}
}
+12
View File
@@ -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} />;
}
+82 -74
View File
@@ -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>
</> </>
)} )}
+7 -8
View File
@@ -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);
+21 -34
View File
@@ -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,
]); ]);
+19 -15
View File
@@ -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>
);
}
+1 -10
View File
@@ -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;
+11 -24
View File
@@ -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,
]); ]);
+1
View File
@@ -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 />
+3 -2
View File
@@ -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>
); );
} }
+2 -8
View File
@@ -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;
} }
} }
} }
+18 -5
View File
@@ -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' })]
: []), : []),
+20 -14
View File
@@ -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';
+1
View File
@@ -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,
+2 -6
View File
@@ -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>(() => {
+3 -1
View File
@@ -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);
+1 -1
View File
@@ -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,
+4 -1
View File
@@ -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",
+1 -1
View File
@@ -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} */