mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-01-11 20:00:29 +01:00
Markdown documentation for HTTP requests (#145)
This commit is contained in:
1560
package-lock.json
generated
1560
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
8
src-tauri/migrations/20241217204951_docs.sql
Normal file
8
src-tauri/migrations/20241217204951_docs.sql
Normal file
@@ -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 =
|
||||
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);
|
||||
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());
|
||||
}
|
||||
info!("Imported {} grpc_requests", imported_resources.grpc_requests.len());
|
||||
@@ -1225,7 +1225,7 @@ async fn cmd_create_grpc_request(
|
||||
) -> Result<GrpcRequest, String> {
|
||||
upsert_grpc_request(
|
||||
&w,
|
||||
&GrpcRequest {
|
||||
GrpcRequest {
|
||||
workspace_id: workspace_id.to_string(),
|
||||
name: name.to_string(),
|
||||
folder_id: folder_id.map(|s| s.to_string()),
|
||||
@@ -1273,7 +1273,7 @@ async fn cmd_update_grpc_request(
|
||||
request: GrpcRequest,
|
||||
w: WebviewWindow,
|
||||
) -> 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]
|
||||
|
||||
@@ -14,7 +14,7 @@ export type Environment = { model: "environment", id: string, workspaceId: strin
|
||||
|
||||
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, };
|
||||
|
||||
@@ -26,9 +26,9 @@ export type GrpcEventType = "info" | "error" | "client_message" | "server_messag
|
||||
|
||||
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, };
|
||||
|
||||
|
||||
@@ -310,6 +310,7 @@ pub struct Folder {
|
||||
pub folder_id: Option<String>,
|
||||
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub sort_priority: f32,
|
||||
}
|
||||
|
||||
@@ -325,6 +326,7 @@ pub enum FolderIden {
|
||||
UpdatedAt,
|
||||
|
||||
Name,
|
||||
Description,
|
||||
SortPriority,
|
||||
}
|
||||
|
||||
@@ -341,6 +343,7 @@ impl<'s> TryFrom<&Row<'s>> for Folder {
|
||||
updated_at: r.get("updated_at")?,
|
||||
folder_id: r.get("folder_id")?,
|
||||
name: r.get("name")?,
|
||||
description: r.get("description")?,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -385,6 +388,7 @@ pub struct HttpRequest {
|
||||
#[ts(type = "Record<string, any>")]
|
||||
pub body: BTreeMap<String, Value>,
|
||||
pub body_type: Option<String>,
|
||||
pub description: String,
|
||||
pub headers: Vec<HttpRequestHeader>,
|
||||
#[serde(default = "default_http_request_method")]
|
||||
pub method: String,
|
||||
@@ -409,6 +413,7 @@ pub enum HttpRequestIden {
|
||||
AuthenticationType,
|
||||
Body,
|
||||
BodyType,
|
||||
Description,
|
||||
Headers,
|
||||
Method,
|
||||
Name,
|
||||
@@ -437,6 +442,7 @@ impl<'s> TryFrom<&Row<'s>> for HttpRequest {
|
||||
method: r.get("method")?,
|
||||
body: serde_json::from_str(body.as_str()).unwrap_or_default(),
|
||||
body_type: r.get("body_type")?,
|
||||
description: r.get("description")?,
|
||||
authentication: serde_json::from_str(authentication.as_str()).unwrap_or_default(),
|
||||
authentication_type: r.get("authentication_type")?,
|
||||
headers: serde_json::from_str(headers.as_str()).unwrap_or_default(),
|
||||
@@ -584,6 +590,7 @@ pub struct GrpcRequest {
|
||||
pub authentication_type: Option<String>,
|
||||
#[ts(type = "Record<string, any>")]
|
||||
pub authentication: BTreeMap<String, Value>,
|
||||
pub description: String,
|
||||
pub message: String,
|
||||
pub metadata: Vec<GrpcMetadataEntry>,
|
||||
pub method: Option<String>,
|
||||
@@ -606,6 +613,7 @@ pub enum GrpcRequestIden {
|
||||
|
||||
Authentication,
|
||||
AuthenticationType,
|
||||
Description,
|
||||
Message,
|
||||
Metadata,
|
||||
Method,
|
||||
@@ -629,6 +637,7 @@ impl<'s> TryFrom<&Row<'s>> for GrpcRequest {
|
||||
updated_at: r.get("updated_at")?,
|
||||
folder_id: r.get("folder_id")?,
|
||||
name: r.get("name")?,
|
||||
description: r.get("description")?,
|
||||
service: r.get("service")?,
|
||||
method: r.get("method")?,
|
||||
message: r.get("message")?,
|
||||
|
||||
@@ -307,7 +307,7 @@ pub async fn duplicate_grpc_request<R: Runtime>(
|
||||
}
|
||||
};
|
||||
request.id = "".to_string();
|
||||
upsert_grpc_request(window, &request).await
|
||||
upsert_grpc_request(window, request).await
|
||||
}
|
||||
|
||||
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>(
|
||||
window: &WebviewWindow<R>,
|
||||
request: &GrpcRequest,
|
||||
request: GrpcRequest,
|
||||
) -> Result<GrpcRequest> {
|
||||
let id = match request.id.as_str() {
|
||||
"" => generate_model_id(ModelType::TypeGrpcRequest),
|
||||
@@ -351,6 +351,7 @@ pub async fn upsert_grpc_request<R: Runtime>(
|
||||
GrpcRequestIden::CreatedAt,
|
||||
GrpcRequestIden::UpdatedAt,
|
||||
GrpcRequestIden::Name,
|
||||
GrpcRequestIden::Description,
|
||||
GrpcRequestIden::WorkspaceId,
|
||||
GrpcRequestIden::FolderId,
|
||||
GrpcRequestIden::SortPriority,
|
||||
@@ -363,17 +364,18 @@ pub async fn upsert_grpc_request<R: Runtime>(
|
||||
GrpcRequestIden::Metadata,
|
||||
])
|
||||
.values_panic([
|
||||
id.as_str().into(),
|
||||
id.into(),
|
||||
CurrentTimestamp.into(),
|
||||
CurrentTimestamp.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.sort_priority.into(),
|
||||
request.url.as_str().into(),
|
||||
request.url.into(),
|
||||
request.service.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(),
|
||||
serde_json::to_string(&request.authentication)?.into(),
|
||||
serde_json::to_string(&request.metadata)?.into(),
|
||||
@@ -384,6 +386,7 @@ pub async fn upsert_grpc_request<R: Runtime>(
|
||||
GrpcRequestIden::UpdatedAt,
|
||||
GrpcRequestIden::WorkspaceId,
|
||||
GrpcRequestIden::Name,
|
||||
GrpcRequestIden::Description,
|
||||
GrpcRequestIden::FolderId,
|
||||
GrpcRequestIden::SortPriority,
|
||||
GrpcRequestIden::Url,
|
||||
@@ -1064,6 +1067,7 @@ pub async fn upsert_folder<R: Runtime>(window: &WebviewWindow<R>, r: Folder) ->
|
||||
FolderIden::WorkspaceId,
|
||||
FolderIden::FolderId,
|
||||
FolderIden::Name,
|
||||
FolderIden::Description,
|
||||
FolderIden::SortPriority,
|
||||
])
|
||||
.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.folder_id.as_ref().map(|s| s.as_str()).into(),
|
||||
trimmed_name.into(),
|
||||
r.description.into(),
|
||||
r.sort_priority.into(),
|
||||
])
|
||||
.on_conflict(
|
||||
@@ -1080,6 +1085,7 @@ pub async fn upsert_folder<R: Runtime>(window: &WebviewWindow<R>, r: Folder) ->
|
||||
.update_columns([
|
||||
FolderIden::UpdatedAt,
|
||||
FolderIden::Name,
|
||||
FolderIden::Description,
|
||||
FolderIden::FolderId,
|
||||
FolderIden::SortPriority,
|
||||
])
|
||||
@@ -1127,6 +1133,7 @@ pub async fn upsert_http_request<R: Runtime>(
|
||||
HttpRequestIden::WorkspaceId,
|
||||
HttpRequestIden::FolderId,
|
||||
HttpRequestIden::Name,
|
||||
HttpRequestIden::Description,
|
||||
HttpRequestIden::Url,
|
||||
HttpRequestIden::UrlParameters,
|
||||
HttpRequestIden::Method,
|
||||
@@ -1141,12 +1148,13 @@ pub async fn upsert_http_request<R: Runtime>(
|
||||
id.as_str().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(),
|
||||
trimmed_name.into(),
|
||||
r.url.as_str().into(),
|
||||
r.description.into(),
|
||||
r.url.into(),
|
||||
serde_json::to_string(&r.url_parameters)?.into(),
|
||||
r.method.as_str().into(),
|
||||
r.method.into(),
|
||||
serde_json::to_string(&r.body)?.into(),
|
||||
r.body_type.as_ref().map(|s| s.as_str()).into(),
|
||||
serde_json::to_string(&r.authentication)?.into(),
|
||||
@@ -1160,6 +1168,7 @@ pub async fn upsert_http_request<R: Runtime>(
|
||||
HttpRequestIden::UpdatedAt,
|
||||
HttpRequestIden::WorkspaceId,
|
||||
HttpRequestIden::Name,
|
||||
HttpRequestIden::Description,
|
||||
HttpRequestIden::FolderId,
|
||||
HttpRequestIden::Method,
|
||||
HttpRequestIden::Headers,
|
||||
|
||||
46
src-web/components/FolderSettingsDialog.tsx
Normal file
46
src-web/components/FolderSettingsDialog.tsx
Normal file
@@ -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 classNames from 'classnames';
|
||||
import type { CSSProperties } from 'react';
|
||||
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { createGlobalState } from 'react-use';
|
||||
import React, { useCallback, useMemo, useRef } from 'react';
|
||||
import { useLocalStorage } from 'react-use';
|
||||
import type { ReflectResponseService } from '../hooks/useGrpc';
|
||||
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
|
||||
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 { BearerAuth } from './BearerAuth';
|
||||
import { Button } from './core/Button';
|
||||
import { CountBadge } from './core/CountBadge';
|
||||
import { Icon } from './core/Icon';
|
||||
import { IconButton } from './core/IconButton';
|
||||
import { PairOrBulkEditor } from './core/PairOrBulkEditor';
|
||||
@@ -20,6 +21,7 @@ import type { TabItem } from './core/Tabs/Tabs';
|
||||
import { TabContent, Tabs } from './core/Tabs/Tabs';
|
||||
import { EmptyStateText } from './EmptyStateText';
|
||||
import { GrpcEditor } from './GrpcEditor';
|
||||
import { MarkdownEditor } from './MarkdownEditor';
|
||||
import { UrlBar } from './UrlBar';
|
||||
|
||||
interface Props {
|
||||
@@ -44,7 +46,10 @@ interface Props {
|
||||
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({
|
||||
style,
|
||||
@@ -61,14 +66,14 @@ export function GrpcConnectionSetupPane({
|
||||
onSend,
|
||||
}: Props) {
|
||||
const updateRequest = useUpdateAnyGrpcRequest();
|
||||
const [activeTab, setActiveTab] = useActiveTab();
|
||||
const [activeTabs, setActiveTabs] = useLocalStorage<Record<string, string>>(
|
||||
'grpcRequestPaneActiveTabs',
|
||||
{},
|
||||
);
|
||||
const { updateKey: forceUpdateKey } = useRequestUpdateKey(activeRequest.id ?? null);
|
||||
|
||||
const [paneSize, setPaneSize] = useState(99999);
|
||||
const urlContainerEl = useRef<HTMLDivElement>(null);
|
||||
useResizeObserver<HTMLDivElement>(urlContainerEl.current, (entry) => {
|
||||
setPaneSize(entry.contentRect.width);
|
||||
});
|
||||
const [paneWidth] = useSize(urlContainerEl.current);
|
||||
|
||||
const handleChangeUrl = useCallback(
|
||||
(url: string) => updateRequest.mutateAsync({ id: activeRequest.id, update: { url } }),
|
||||
@@ -129,9 +134,18 @@ export function GrpcConnectionSetupPane({
|
||||
|
||||
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',
|
||||
options: {
|
||||
value: activeRequest.authenticationType,
|
||||
@@ -160,29 +174,44 @@ export function GrpcConnectionSetupPane({
|
||||
},
|
||||
},
|
||||
},
|
||||
{ value: 'metadata', label: 'Metadata' },
|
||||
{ value: TAB_METADATA, label: 'Metadata' },
|
||||
],
|
||||
[
|
||||
activeRequest.authentication,
|
||||
activeRequest.authenticationType,
|
||||
activeRequest.description,
|
||||
activeRequest.id,
|
||||
updateRequest,
|
||||
],
|
||||
);
|
||||
|
||||
const activeTab = activeTabs?.[activeRequest.id];
|
||||
const setActiveTab = useCallback(
|
||||
(tab: string) => {
|
||||
setActiveTabs((r) => ({ ...r, [activeRequest.id]: tab }));
|
||||
},
|
||||
[activeRequest.id, setActiveTabs],
|
||||
);
|
||||
|
||||
const handleMetadataChange = useCallback(
|
||||
(metadata: GrpcMetadataEntry[]) =>
|
||||
updateRequest.mutate({ id: activeRequest.id, update: { metadata } }),
|
||||
[activeRequest.id, updateRequest],
|
||||
);
|
||||
|
||||
const handleDescriptionChange = useCallback(
|
||||
(description: string) =>
|
||||
updateRequest.mutate({ id: activeRequest.id, update: { description } }),
|
||||
[activeRequest.id, updateRequest],
|
||||
);
|
||||
|
||||
return (
|
||||
<VStack style={style}>
|
||||
<div
|
||||
ref={urlContainerEl}
|
||||
className={classNames(
|
||||
'grid grid-cols-[minmax(0,1fr)_auto] gap-1.5',
|
||||
paneSize < 400 && '!grid-cols-1',
|
||||
paneWidth < 400 && '!grid-cols-1',
|
||||
)}
|
||||
>
|
||||
<UrlBar
|
||||
@@ -222,7 +251,7 @@ export function GrpcConnectionSetupPane({
|
||||
disabled={isStreaming || services == null}
|
||||
className={classNames(
|
||||
'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'}
|
||||
@@ -312,6 +341,14 @@ export function GrpcConnectionSetupPane({
|
||||
forceUpdateKey={forceUpdateKey}
|
||||
/>
|
||||
</TabContent>
|
||||
<TabContent value={TAB_DESCRIPTION}>
|
||||
<MarkdownEditor
|
||||
name="request-description"
|
||||
placeholder="A Markdown description of this request."
|
||||
defaultValue={activeRequest.description}
|
||||
onChange={handleDescriptionChange}
|
||||
/>
|
||||
</TabContent>
|
||||
</Tabs>
|
||||
</VStack>
|
||||
);
|
||||
|
||||
137
src-web/components/MarkdownEditor.tsx
Normal file
137
src-web/components/MarkdownEditor.tsx
Normal 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
src-web/components/Prose.css
Normal file
201
src-web/components/Prose.css
Normal 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
src-web/components/Prose.tsx
Normal file
12
src-web/components/Prose.tsx
Normal 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} />;
|
||||
}
|
||||
@@ -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 type { CSSProperties } 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 { useUpdateAnyHttpRequest } from '../hooks/useUpdateAnyHttpRequest';
|
||||
import { languageFromContentType } from '../lib/contentType';
|
||||
import { fallbackRequestName } from '../lib/fallbackRequestName';
|
||||
import { tryFormatJson } from '../lib/formatters';
|
||||
import {
|
||||
AUTH_TYPE_BASIC,
|
||||
@@ -34,9 +35,13 @@ import { BearerAuth } from './BearerAuth';
|
||||
import { BinaryFileEditor } from './BinaryFileEditor';
|
||||
import { CountBadge } from './core/CountBadge';
|
||||
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 type { Pair } from './core/PairEditor';
|
||||
import { PlainInput } from './core/PlainInput';
|
||||
import type { TabItem } from './core/Tabs/Tabs';
|
||||
import { TabContent, Tabs } from './core/Tabs/Tabs';
|
||||
import { EmptyStateText } from './EmptyStateText';
|
||||
@@ -44,6 +49,7 @@ import { FormMultipartEditor } from './FormMultipartEditor';
|
||||
import { FormUrlencodedEditor } from './FormUrlencodedEditor';
|
||||
import { GraphQLEditor } from './GraphQLEditor';
|
||||
import { HeadersEditor } from './HeadersEditor';
|
||||
import { MarkdownEditor } from './MarkdownEditor';
|
||||
import { useToast } from './ToastContext';
|
||||
import { UrlBar } from './UrlBar';
|
||||
import { UrlParametersEditor } from './UrlParameterEditor';
|
||||
@@ -59,8 +65,7 @@ const TAB_BODY = 'body';
|
||||
const TAB_PARAMS = 'params';
|
||||
const TAB_HEADERS = 'headers';
|
||||
const TAB_AUTH = 'auth';
|
||||
|
||||
const DEFAULT_TAB = TAB_BODY;
|
||||
const TAB_DESCRIPTION = 'description';
|
||||
|
||||
export const RequestPane = memo(function RequestPane({
|
||||
style,
|
||||
@@ -120,6 +125,15 @@ export const RequestPane = memo(function RequestPane({
|
||||
|
||||
const tabs: TabItem[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
value: TAB_DESCRIPTION,
|
||||
label: (
|
||||
<div className="flex items-center">
|
||||
Docs
|
||||
{activeRequest.description && <CountBadge count={true} />}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
value: TAB_BODY,
|
||||
options: {
|
||||
@@ -239,68 +253,36 @@ export const RequestPane = memo(function RequestPane({
|
||||
activeRequest.authentication,
|
||||
activeRequest.authenticationType,
|
||||
activeRequest.bodyType,
|
||||
activeRequest.description,
|
||||
activeRequest.headers,
|
||||
activeRequest.method,
|
||||
activeRequestId,
|
||||
handleContentTypeChange,
|
||||
toast,
|
||||
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(
|
||||
(body: HttpRequest['body']) => updateRequest.mutate({ id: activeRequestId, update: { body } }),
|
||||
[activeRequestId, updateRequest],
|
||||
);
|
||||
|
||||
const handleBinaryFileChange = useCallback(
|
||||
(body: HttpRequest['body']) => {
|
||||
updateRequest.mutate({ id: activeRequestId, update: { body } });
|
||||
},
|
||||
[activeRequestId, updateRequest],
|
||||
);
|
||||
const handleBodyTextChange = useCallback(
|
||||
(text: string) => updateRequest.mutate({ id: activeRequestId, update: { body: { text } } }),
|
||||
[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 { 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 activeTab = activeTabs?.[activeRequestId];
|
||||
const setActiveTab = useCallback(
|
||||
(tab: string) => {
|
||||
setActiveTabs((r) => ({ ...r, [activeRequest.id]: tab }));
|
||||
@@ -312,6 +294,21 @@ export const RequestPane = memo(function RequestPane({
|
||||
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 (
|
||||
<div
|
||||
style={style}
|
||||
@@ -332,30 +329,15 @@ export const RequestPane = memo(function RequestPane({
|
||||
importQuerystring.mutate(text);
|
||||
}
|
||||
}}
|
||||
autocomplete={{
|
||||
minMatch: 3,
|
||||
options:
|
||||
requests.length > 0
|
||||
? [
|
||||
...requests
|
||||
.filter((r) => r.id !== activeRequestId)
|
||||
.map(
|
||||
(r) =>
|
||||
({
|
||||
type: 'constant',
|
||||
label: r.url,
|
||||
}) as GenericCompletionOption,
|
||||
),
|
||||
]
|
||||
: [
|
||||
{ label: 'http://', type: 'constant' },
|
||||
{ label: 'https://', type: 'constant' },
|
||||
],
|
||||
}}
|
||||
onSend={handleSend}
|
||||
onCancel={handleCancel}
|
||||
onMethodChange={handleMethodChange}
|
||||
onUrlChange={handleUrlChange}
|
||||
autocomplete={autocomplete}
|
||||
onSend={() => sendRequest.mutateAsync(activeRequest.id ?? null)}
|
||||
onCancel={cancelResponse.mutate}
|
||||
onMethodChange={(method) =>
|
||||
updateRequest.mutate({ id: activeRequestId, update: { method } })
|
||||
}
|
||||
onUrlChange={(url: string) =>
|
||||
updateRequest.mutate({ id: activeRequestId, update: { url } })
|
||||
}
|
||||
forceUpdateKey={updateKey}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
@@ -382,14 +364,18 @@ export const RequestPane = memo(function RequestPane({
|
||||
<HeadersEditor
|
||||
forceUpdateKey={`${forceUpdateHeaderEditorKey}::${forceUpdateKey}`}
|
||||
headers={activeRequest.headers}
|
||||
onChange={handleHeadersChange}
|
||||
onChange={(headers) =>
|
||||
updateRequest.mutate({ id: activeRequestId, update: { headers } })
|
||||
}
|
||||
/>
|
||||
</TabContent>
|
||||
<TabContent value={TAB_PARAMS}>
|
||||
<UrlParametersEditor
|
||||
forceUpdateKey={forceUpdateKey + urlParametersKey}
|
||||
pairs={urlParameterPairs}
|
||||
onChange={handleUrlParametersChange}
|
||||
onChange={(urlParameters) =>
|
||||
updateRequest.mutate({ id: activeRequestId, update: { urlParameters } })
|
||||
}
|
||||
/>
|
||||
</TabContent>
|
||||
<TabContent value={TAB_BODY}>
|
||||
@@ -440,7 +426,9 @@ export const RequestPane = memo(function RequestPane({
|
||||
requestId={activeRequest.id}
|
||||
contentType={contentType}
|
||||
body={activeRequest.body}
|
||||
onChange={handleBinaryFileChange}
|
||||
onChange={(body) =>
|
||||
updateRequest.mutate({ id: activeRequestId, update: { body } })
|
||||
}
|
||||
onChangeContentType={handleContentTypeChange}
|
||||
/>
|
||||
) : typeof activeRequest.bodyType === 'string' ? (
|
||||
@@ -458,6 +446,26 @@ export const RequestPane = memo(function RequestPane({
|
||||
<EmptyStateText>Empty Body</EmptyStateText>
|
||||
)}
|
||||
</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>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -38,7 +38,6 @@ interface Props {
|
||||
const TAB_BODY = 'body';
|
||||
const TAB_HEADERS = 'headers';
|
||||
const TAB_INFO = 'info';
|
||||
const DEFAULT_TAB = TAB_BODY;
|
||||
|
||||
export const ResponsePane = memo(function ResponsePane({ style, className, activeRequest }: Props) {
|
||||
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 activeTab = activeTabs?.[activeRequest.id] ?? DEFAULT_TAB;
|
||||
const setActiveTab = useCallback(
|
||||
(tab: string) => {
|
||||
setActiveTabs((r) => ({ ...r, [activeRequest.id]: tab }));
|
||||
},
|
||||
[activeRequest.id, setActiveTabs],
|
||||
);
|
||||
|
||||
const tabs = useMemo<TabItem[]>(
|
||||
() => [
|
||||
@@ -88,6 +80,13 @@ export const ResponsePane = memo(function ResponsePane({ style, className, activ
|
||||
],
|
||||
[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);
|
||||
|
||||
|
||||
@@ -32,7 +32,6 @@ import { useHttpRequestActions } from '../hooks/useHttpRequestActions';
|
||||
import { useHttpResponses } from '../hooks/useHttpResponses';
|
||||
import { useKeyValue } from '../hooks/useKeyValue';
|
||||
import { useMoveToWorkspace } from '../hooks/useMoveToWorkspace';
|
||||
import { usePrompt } from '../hooks/usePrompt';
|
||||
import { useRenameRequest } from '../hooks/useRenameRequest';
|
||||
import { useRequests } from '../hooks/useRequests';
|
||||
import { useScrollIntoView } from '../hooks/useScrollIntoView';
|
||||
@@ -50,10 +49,11 @@ import type { DropdownItem } from './core/Dropdown';
|
||||
import { ContextMenu } from './core/Dropdown';
|
||||
import { HttpMethodTag } from './core/HttpMethodTag';
|
||||
import { Icon } from './core/Icon';
|
||||
import { InlineCode } from './core/InlineCode';
|
||||
import { VStack } from './core/Stacks';
|
||||
import { StatusTag } from './core/StatusTag';
|
||||
import { useDialog } from './DialogContext';
|
||||
import { DropMarker } from './DropMarker';
|
||||
import { FolderSettingsDialog } from './FolderSettingsDialog';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
@@ -694,6 +694,7 @@ function SidebarItem({
|
||||
|
||||
connectDrag(connectDrop(ref));
|
||||
|
||||
const dialog = useDialog();
|
||||
const activeRequest = useActiveRequest();
|
||||
const deleteFolder = useDeleteFolder(itemId);
|
||||
const deleteRequest = useDeleteRequest(itemId);
|
||||
@@ -706,8 +707,6 @@ function SidebarItem({
|
||||
const updateHttpRequest = useUpdateAnyHttpRequest();
|
||||
const workspaces = useWorkspaces();
|
||||
const updateGrpcRequest = useUpdateAnyGrpcRequest();
|
||||
const updateAnyFolder = useUpdateAnyFolder();
|
||||
const prompt = usePrompt();
|
||||
const [editing, setEditing] = useState<boolean>(false);
|
||||
const isActive = activeRequest?.id === itemId;
|
||||
const createDropdownItems = useCreateDropdownItems({ folderId: itemId });
|
||||
@@ -786,35 +785,25 @@ function SidebarItem({
|
||||
if (itemModel === 'folder') {
|
||||
return [
|
||||
{
|
||||
key: 'sendAll',
|
||||
key: 'send-all',
|
||||
label: 'Send All',
|
||||
leftSlot: <Icon icon="send_horizontal" />,
|
||||
onSelect: () => sendManyRequests.mutate(child.children.map((c) => c.item.id)),
|
||||
},
|
||||
{
|
||||
key: 'rename',
|
||||
label: 'Rename',
|
||||
leftSlot: <Icon icon="pencil" />,
|
||||
onSelect: async () => {
|
||||
const name = await prompt({
|
||||
id: 'rename-folder',
|
||||
title: 'Rename Folder',
|
||||
description: (
|
||||
<>
|
||||
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: 'folder-settings',
|
||||
label: 'Settings',
|
||||
leftSlot: <Icon icon="settings" />,
|
||||
onSelect: () =>
|
||||
dialog.show({
|
||||
id: 'folder-settings',
|
||||
title: 'Folder Settings',
|
||||
size: 'md',
|
||||
render: () => <FolderSettingsDialog folderId={itemId} />,
|
||||
}),
|
||||
},
|
||||
{
|
||||
key: 'deleteFolder',
|
||||
key: 'delete-folder',
|
||||
label: 'Delete',
|
||||
variant: 'danger',
|
||||
leftSlot: <Icon icon="trash" />,
|
||||
@@ -828,7 +817,7 @@ function SidebarItem({
|
||||
itemModel === 'http_request'
|
||||
? [
|
||||
{
|
||||
key: 'sendRequest',
|
||||
key: 'send-request',
|
||||
label: 'Send',
|
||||
hotKeyAction: 'http_request.send',
|
||||
hotKeyLabelOnly: true, // Already bound in URL bar
|
||||
@@ -851,13 +840,13 @@ function SidebarItem({
|
||||
return [
|
||||
...requestItems,
|
||||
{
|
||||
key: 'renameRequest',
|
||||
key: 'rename-request',
|
||||
label: 'Rename',
|
||||
leftSlot: <Icon icon="pencil" />,
|
||||
onSelect: renameRequest.mutate,
|
||||
},
|
||||
{
|
||||
key: 'duplicateRequest',
|
||||
key: 'duplicate-request',
|
||||
label: 'Duplicate',
|
||||
hotKeyAction: 'http_request.duplicate',
|
||||
hotKeyLabelOnly: true, // Would trigger for every request (bad)
|
||||
@@ -868,14 +857,14 @@ function SidebarItem({
|
||||
: duplicateGrpcRequest.mutate(),
|
||||
},
|
||||
{
|
||||
key: 'moveWorkspace',
|
||||
key: 'move-workspace',
|
||||
label: 'Move',
|
||||
leftSlot: <Icon icon="arrow_right_circle" />,
|
||||
hidden: workspaces.length <= 1,
|
||||
onSelect: moveToWorkspace.mutate,
|
||||
},
|
||||
{
|
||||
key: 'deleteRequest',
|
||||
key: 'delete-request',
|
||||
variant: 'danger',
|
||||
label: 'Delete',
|
||||
leftSlot: <Icon icon="trash" />,
|
||||
@@ -888,18 +877,16 @@ function SidebarItem({
|
||||
createDropdownItems,
|
||||
deleteFolder,
|
||||
deleteRequest,
|
||||
dialog,
|
||||
duplicateGrpcRequest,
|
||||
duplicateHttpRequest,
|
||||
httpRequestActions,
|
||||
itemId,
|
||||
itemModel,
|
||||
itemName,
|
||||
moveToWorkspace.mutate,
|
||||
prompt,
|
||||
renameRequest.mutate,
|
||||
sendManyRequests,
|
||||
sendRequest,
|
||||
updateAnyFolder,
|
||||
workspaces.length,
|
||||
]);
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ export const UrlBar = memo(function UrlBar({
|
||||
<Input
|
||||
autocompleteVariables
|
||||
ref={inputRef}
|
||||
size="sm"
|
||||
size="md"
|
||||
wrapLines={isFocused}
|
||||
hideLabel
|
||||
useTemplating
|
||||
@@ -86,26 +86,30 @@ export const UrlBar = memo(function UrlBar({
|
||||
leftSlot={
|
||||
method != null &&
|
||||
onMethodChange != null && (
|
||||
<RequestMethodDropdown
|
||||
method={method}
|
||||
onChange={onMethodChange}
|
||||
className="my-0.5 ml-0.5"
|
||||
/>
|
||||
<div className="py-0.5">
|
||||
<RequestMethodDropdown
|
||||
method={method}
|
||||
onChange={onMethodChange}
|
||||
className="ml-0.5 !h-full"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
rightSlot={
|
||||
<>
|
||||
{rightSlot}
|
||||
{submitIcon !== null && (
|
||||
<IconButton
|
||||
size="xs"
|
||||
iconSize="md"
|
||||
title="Send Request"
|
||||
type="submit"
|
||||
className="w-8 my-0.5 mr-0.5"
|
||||
icon={isLoading ? 'x' : submitIcon}
|
||||
hotkeyAction="http_request.send"
|
||||
/>
|
||||
<div className="py-0.5">
|
||||
<IconButton
|
||||
size="xs"
|
||||
iconSize="md"
|
||||
title="Send Request"
|
||||
type="submit"
|
||||
className="w-8 mr-0.5 !h-full"
|
||||
icon={isLoading ? 'x' : submitIcon}
|
||||
hotkeyAction="http_request.send"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
|
||||
39
src-web/components/WorkpaceSettingsDialog.tsx
Normal file
39
src-web/components/WorkpaceSettingsDialog.tsx
Normal file
@@ -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 type { CSSProperties, MouseEvent as ReactMouseEvent } from 'react';
|
||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { useWindowSize } from 'react-use';
|
||||
import { useActiveRequest } from '../hooks/useActiveRequest';
|
||||
import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
|
||||
import { useFloatingSidebarHidden } from '../hooks/useFloatingSidebarHidden';
|
||||
@@ -34,13 +33,13 @@ const drag = { gridArea: 'drag' };
|
||||
|
||||
export default function Workspace() {
|
||||
useSyncWorkspaceRequestTitle();
|
||||
|
||||
const workspaces = useWorkspaces();
|
||||
const activeWorkspace = useActiveWorkspace();
|
||||
const { setWidth, width, resetWidth } = useSidebarWidth();
|
||||
const [sidebarHidden, setSidebarHidden] = useSidebarHidden();
|
||||
const [floatingSidebarHidden, setFloatingSidebarHidden] = useFloatingSidebarHidden();
|
||||
const activeRequest = useActiveRequest();
|
||||
const windowSize = useWindowSize();
|
||||
const importData = useImportData();
|
||||
const floating = useShouldFloatSidebar();
|
||||
const [isResizing, setIsResizing] = useState<boolean>(false);
|
||||
@@ -103,14 +102,6 @@ export default function Workspace() {
|
||||
[sideWidth, floating],
|
||||
);
|
||||
|
||||
if (windowSize.width <= 100) {
|
||||
return (
|
||||
<div>
|
||||
<Button>Send</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// We're loading still
|
||||
if (workspaces.length === 0) {
|
||||
return null;
|
||||
|
||||
@@ -5,20 +5,18 @@ import { useCreateWorkspace } from '../hooks/useCreateWorkspace';
|
||||
import { useDeleteSendHistory } from '../hooks/useDeleteSendHistory';
|
||||
import { useDeleteWorkspace } from '../hooks/useDeleteWorkspace';
|
||||
import { useOpenWorkspace } from '../hooks/useOpenWorkspace';
|
||||
import { usePrompt } from '../hooks/usePrompt';
|
||||
import { useSettings } from '../hooks/useSettings';
|
||||
import { useUpdateWorkspace } from '../hooks/useUpdateWorkspace';
|
||||
import { useWorkspaces } from '../hooks/useWorkspaces';
|
||||
import { getWorkspace } from '../lib/store';
|
||||
import type { ButtonProps } from './core/Button';
|
||||
import { Button } from './core/Button';
|
||||
import type { DropdownItem } from './core/Dropdown';
|
||||
import { Icon } from './core/Icon';
|
||||
import { InlineCode } from './core/InlineCode';
|
||||
import type { RadioDropdownItem } from './core/RadioDropdown';
|
||||
import { RadioDropdown } from './core/RadioDropdown';
|
||||
import { useDialog } from './DialogContext';
|
||||
import { OpenWorkspaceDialog } from './OpenWorkspaceDialog';
|
||||
import { WorkspaceSettingsDialog } from './WorkpaceSettingsDialog';
|
||||
|
||||
type Props = Pick<ButtonProps, 'className' | 'justify' | 'forDropdown' | 'leftSlot'>;
|
||||
|
||||
@@ -29,11 +27,9 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
|
||||
const workspaces = useWorkspaces();
|
||||
const activeWorkspace = useActiveWorkspace();
|
||||
const activeWorkspaceId = activeWorkspace?.id ?? null;
|
||||
const updateWorkspace = useUpdateWorkspace(activeWorkspaceId);
|
||||
const deleteWorkspace = useDeleteWorkspace(activeWorkspace);
|
||||
const createWorkspace = useCreateWorkspace();
|
||||
const dialog = useDialog();
|
||||
const prompt = usePrompt();
|
||||
const settings = useSettings();
|
||||
const openWorkspace = useOpenWorkspace();
|
||||
const openWorkspaceNewWindow = settings?.openWorkspaceNewWindow ?? null;
|
||||
@@ -52,24 +48,16 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
|
||||
|
||||
const extraItems: DropdownItem[] = [
|
||||
{
|
||||
key: 'rename',
|
||||
label: 'Rename',
|
||||
leftSlot: <Icon icon="pencil" />,
|
||||
key: 'workspace-settings',
|
||||
label: 'Settings',
|
||||
leftSlot: <Icon icon="settings" />,
|
||||
onSelect: async () => {
|
||||
const name = await prompt({
|
||||
id: 'rename-workspace',
|
||||
title: 'Rename Workspace',
|
||||
description: (
|
||||
<>
|
||||
Enter a new name for <InlineCode>{activeWorkspace?.name}</InlineCode>
|
||||
</>
|
||||
),
|
||||
label: 'Name',
|
||||
placeholder: 'New Name',
|
||||
defaultValue: activeWorkspace?.name,
|
||||
dialog.show({
|
||||
id: 'workspace-settings',
|
||||
title: 'Workspace Settings',
|
||||
size: 'md',
|
||||
render: () => <WorkspaceSettingsDialog workspaceId={activeWorkspace?.id ?? null} />,
|
||||
});
|
||||
if (name == null) return;
|
||||
updateWorkspace.mutate({ name });
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -96,13 +84,12 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
|
||||
|
||||
return { workspaceItems, extraItems };
|
||||
}, [
|
||||
activeWorkspace?.name,
|
||||
activeWorkspace,
|
||||
activeWorkspaceId,
|
||||
createWorkspace.mutate,
|
||||
deleteSendHistory.mutate,
|
||||
deleteWorkspace.mutate,
|
||||
prompt,
|
||||
updateWorkspace,
|
||||
dialog,
|
||||
workspaces,
|
||||
]);
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ export const WorkspaceHeader = memo(function WorkspaceHeader({ className }: Prop
|
||||
icon="search"
|
||||
title="Search or execute a command"
|
||||
size="sm"
|
||||
event="search"
|
||||
onClick={togglePalette}
|
||||
/>
|
||||
<SettingsDropdown />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import classNames from 'classnames';
|
||||
|
||||
interface Props {
|
||||
count: number;
|
||||
count: number | true;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
@@ -12,10 +12,11 @@ export function CountBadge({ count, className }: Props) {
|
||||
aria-hidden
|
||||
className={classNames(
|
||||
className,
|
||||
'flex items-center',
|
||||
'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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
.cm-line {
|
||||
@apply w-full;
|
||||
/* 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 {
|
||||
@@ -51,7 +51,7 @@
|
||||
/* Style 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
|
||||
through. Move left slightly to fix that. */
|
||||
@apply -left-[1px];
|
||||
@@ -114,12 +114,6 @@
|
||||
|
||||
.cm-scroller {
|
||||
@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 type { MutableRefObject, ReactNode } from 'react';
|
||||
import {
|
||||
useEffect,
|
||||
Children,
|
||||
cloneElement,
|
||||
forwardRef,
|
||||
isValidElement,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useMemo,
|
||||
useRef,
|
||||
@@ -45,7 +45,16 @@ export interface EditorProps {
|
||||
type?: 'text' | 'password';
|
||||
className?: string;
|
||||
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;
|
||||
autoFocus?: boolean;
|
||||
autoSelect?: boolean;
|
||||
@@ -66,6 +75,7 @@ export interface EditorProps {
|
||||
autocompleteVariables?: boolean;
|
||||
extraExtensions?: Extension[];
|
||||
actions?: ReactNode;
|
||||
hideGutter?: boolean;
|
||||
}
|
||||
|
||||
const emptyVariables: EnvironmentVariable[] = [];
|
||||
@@ -96,6 +106,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
|
||||
autocompleteVariables,
|
||||
actions,
|
||||
wrapLines,
|
||||
hideGutter,
|
||||
}: EditorProps,
|
||||
ref,
|
||||
) {
|
||||
@@ -310,6 +321,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
|
||||
container,
|
||||
readOnly,
|
||||
singleLine,
|
||||
hideGutter,
|
||||
onChange: handleChange,
|
||||
onPaste: handlePaste,
|
||||
onPasteOverwrite: handlePasteOverwrite,
|
||||
@@ -374,7 +386,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
|
||||
const decoratedActions = useMemo(() => {
|
||||
const results = [];
|
||||
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) {
|
||||
@@ -455,13 +467,14 @@ function getExtensions({
|
||||
container,
|
||||
readOnly,
|
||||
singleLine,
|
||||
hideGutter,
|
||||
onChange,
|
||||
onPaste,
|
||||
onPasteOverwrite,
|
||||
onFocus,
|
||||
onBlur,
|
||||
onKeyDown,
|
||||
}: Pick<EditorProps, 'singleLine' | 'readOnly'> & {
|
||||
}: Pick<EditorProps, 'singleLine' | 'readOnly' | 'hideGutter'> & {
|
||||
container: HTMLDivElement | null;
|
||||
onChange: MutableRefObject<EditorProps['onChange']>;
|
||||
onPaste: MutableRefObject<EditorProps['onPaste']>;
|
||||
@@ -499,7 +512,7 @@ function getExtensions({
|
||||
tooltips({ parent }),
|
||||
keymap.of(singleLine ? defaultKeymap.filter((k) => k.key !== 'Enter') : defaultKeymap),
|
||||
...(singleLine ? [singleLineExt()] : []),
|
||||
...(!singleLine ? [multiLineExtensions] : []),
|
||||
...(!singleLine ? [multiLineExtensions({ hideGutter })] : []),
|
||||
...(readOnly
|
||||
? [EditorState.readOnly.of(true), EditorView.contentAttributes.of({ tabindex: '-1' })]
|
||||
: []),
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
} from '@codemirror/autocomplete';
|
||||
import { history, historyKeymap, indentWithTab } from '@codemirror/commands';
|
||||
import { javascript } from '@codemirror/lang-javascript';
|
||||
import { markdown } from '@codemirror/lang-markdown';
|
||||
import { json } from '@codemirror/lang-json';
|
||||
import { xml } from '@codemirror/lang-xml';
|
||||
import type { LanguageSupport } from '@codemirror/language';
|
||||
@@ -79,6 +80,7 @@ const syntaxExtensions: Record<NonNullable<EditorProps['language']>, LanguageSup
|
||||
url: url(),
|
||||
pairs: pairs(),
|
||||
text: text(),
|
||||
markdown: markdown(),
|
||||
};
|
||||
|
||||
export function getLanguageExtension({
|
||||
@@ -138,21 +140,25 @@ export const baseExtensions = [
|
||||
keymap.of([...historyKeymap, ...completionKeymap]),
|
||||
];
|
||||
|
||||
export const multiLineExtensions = [
|
||||
lineNumbers(),
|
||||
foldGutter({
|
||||
markerDOM: (open) => {
|
||||
const el = document.createElement('div');
|
||||
el.classList.add('fold-gutter-icon');
|
||||
el.tabIndex = -1;
|
||||
if (open) {
|
||||
el.setAttribute('data-open', '');
|
||||
}
|
||||
return el;
|
||||
},
|
||||
}),
|
||||
export const multiLineExtensions = ({ hideGutter }: { hideGutter?: boolean }) => [
|
||||
hideGutter
|
||||
? []
|
||||
: [
|
||||
lineNumbers(),
|
||||
foldGutter({
|
||||
markerDOM: (open) => {
|
||||
const el = document.createElement('div');
|
||||
el.classList.add('fold-gutter-icon');
|
||||
el.tabIndex = -1;
|
||||
if (open) {
|
||||
el.setAttribute('data-open', '');
|
||||
}
|
||||
return el;
|
||||
},
|
||||
}),
|
||||
],
|
||||
codeFolding({
|
||||
placeholderDOM(view, onclick, prepared) {
|
||||
placeholderDOM(_view, onclick, prepared) {
|
||||
const el = document.createElement('span');
|
||||
el.onclick = onclick;
|
||||
el.className = 'cm-foldPlaceholder';
|
||||
|
||||
@@ -30,6 +30,7 @@ const icons = {
|
||||
circle_alert: lucide.CircleAlertIcon,
|
||||
clock: lucide.ClockIcon,
|
||||
code: lucide.CodeIcon,
|
||||
columns_2: lucide.Columns2Icon,
|
||||
cookie: lucide.CookieIcon,
|
||||
copy: lucide.CopyIcon,
|
||||
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 type { CSSProperties, MouseEvent as ReactMouseEvent, ReactNode } from 'react';
|
||||
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
||||
@@ -43,7 +43,6 @@ export function SplitLayout({
|
||||
}: Props) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const activeWorkspace = useActiveWorkspace();
|
||||
const [verticalBasedOnSize, setVerticalBasedOnSize] = useState<boolean>(false);
|
||||
const [widthRaw, setWidth] = useLocalStorage<number>(
|
||||
`${name}_width::${activeWorkspace?.id ?? 'n/a'}`,
|
||||
);
|
||||
@@ -62,10 +61,7 @@ export function SplitLayout({
|
||||
minHeightPx = 0;
|
||||
}
|
||||
|
||||
useResizeObserver(containerRef.current, ({ contentRect }) => {
|
||||
setVerticalBasedOnSize(contentRect.width < STACK_VERTICAL_WIDTH);
|
||||
});
|
||||
|
||||
const verticalBasedOnSize = useSize(containerRef.current)[0] < STACK_VERTICAL_WIDTH;
|
||||
const vertical = layout !== 'horizontal' && (layout === 'vertical' || verticalBasedOnSize);
|
||||
|
||||
const styles = useMemo<CSSProperties>(() => {
|
||||
|
||||
@@ -40,6 +40,8 @@ export function Tabs({
|
||||
}: Props) {
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
value = value ?? tabs[0]?.value;
|
||||
|
||||
// Update tabs when value changes
|
||||
useEffect(() => {
|
||||
const tabs = ref.current?.querySelectorAll<HTMLDivElement>(`[data-tab]`);
|
||||
@@ -61,7 +63,7 @@ export function Tabs({
|
||||
return (
|
||||
<div
|
||||
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
|
||||
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/AnnotationLayer.css';
|
||||
import { convertFileSrc } from '@tauri-apps/api/core';
|
||||
@@ -6,7 +5,7 @@ import './PdfViewer.css';
|
||||
import type { PDFDocumentProxy } from 'pdfjs-dist';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { Document, Page } from 'react-pdf';
|
||||
import { useDebouncedState } from '../../hooks/useDebouncedState';
|
||||
import useSize from '@react-hook/size';
|
||||
|
||||
interface Props {
|
||||
bodyPath: string;
|
||||
@@ -19,12 +18,9 @@ const options = {
|
||||
|
||||
export function PdfViewer({ bodyPath }: Props) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [containerWidth, setContainerWidth] = useDebouncedState<number>(0, 100);
|
||||
const [numPages, setNumPages] = useState<number>();
|
||||
|
||||
useResizeObserver(containerRef.current ?? null, (v) => {
|
||||
setContainerWidth(v.contentRect.width);
|
||||
});
|
||||
const [containerWidth] = useSize(containerRef.current);
|
||||
|
||||
const onDocumentLoadSuccess = ({ numPages: nextNumPages }: PDFDocumentProxy): void => {
|
||||
setNumPages(nextNumPages);
|
||||
|
||||
@@ -69,7 +69,7 @@ function themeVariables(theme?: Partial<YaakColors>, base?: CSSVariables): CSSVa
|
||||
theme?.shadow ??
|
||||
YaakColor.black().translucify(isThemeDark(theme ?? ({} as Partial<YaakColors>)) ? 0.7 : 0.93),
|
||||
primary: theme?.primary,
|
||||
secondary: theme?.primary,
|
||||
secondary: theme?.secondary,
|
||||
info: theme?.info,
|
||||
success: theme?.success,
|
||||
notice: theme?.notice,
|
||||
|
||||
@@ -12,12 +12,13 @@
|
||||
"@codemirror/commands": "6.7.0",
|
||||
"@codemirror/lang-javascript": "^6.2.2",
|
||||
"@codemirror/lang-json": "^6.0.1",
|
||||
"@codemirror/lang-markdown": "^6.3.1",
|
||||
"@codemirror/lang-xml": "^6.0.2",
|
||||
"@codemirror/language": "^6.6.0",
|
||||
"@codemirror/search": "^6.2.3",
|
||||
"@lezer/highlight": "^1.1.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",
|
||||
"@tanstack/react-query": "^5.59.16",
|
||||
"@tanstack/react-virtual": "^3.10.8",
|
||||
@@ -49,9 +50,11 @@
|
||||
"react-dnd-html5-backend": "^16.0.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-helmet-async": "^2.0.5",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-pdf": "^9.1.0",
|
||||
"react-router-dom": "^6.26.2",
|
||||
"react-use": "^17.5.1",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"slugify": "^1.6.6",
|
||||
"uuid": "^10.0.0",
|
||||
"whatwg-mimetype": "^4.0.0",
|
||||
|
||||
@@ -4,7 +4,7 @@ const sizes = {
|
||||
'2xs': '1.4rem',
|
||||
xs: '1.8rem',
|
||||
sm: '2.0rem',
|
||||
md: '2.5rem',
|
||||
md: '2.3rem',
|
||||
};
|
||||
|
||||
/** @type {import("tailwindcss").Config} */
|
||||
|
||||
Reference in New Issue
Block a user