Skip to content

Commit

Permalink
Multi-line multi-part values
Browse files Browse the repository at this point in the history
  • Loading branch information
gschier committed Jan 27, 2025
1 parent 1d37a15 commit 662c38d
Show file tree
Hide file tree
Showing 11 changed files with 147 additions and 40 deletions.
20 changes: 20 additions & 0 deletions packages/common-lib/formatSize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export function formatSize(bytes: number): string {
let num;
let unit;

if (bytes > 1000 * 1000 * 1000) {
num = bytes / 1000 / 1000 / 1000;
unit = 'GB';
} else if (bytes > 1000 * 1000) {
num = bytes / 1000 / 1000;
unit = 'MB';
} else if (bytes > 1000) {
num = bytes / 1000;
unit = 'KB';
} else {
num = bytes;
unit = 'B';
}

return `${Math.round(num * 10) / 10} ${unit}`;
}
25 changes: 22 additions & 3 deletions src-tauri/src/http_request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -326,14 +326,33 @@ pub async fn send_http_request<R: Runtime>(

// Set or guess mimetype
if !content_type.is_empty() {
part = part.mime_str(content_type).map_err(|e| e.to_string())?;
part = match part.mime_str(content_type) {
Ok(p) => p,
Err(e) => {
return Ok(response_err(
&*response.lock().await,
format!("Invalid mime for multi-part entry {e:?}"),
window,
)
.await);
}
};
} else if !file_path.is_empty() {
let default_mime =
Mime::from_str("application/octet-stream").unwrap();
let mime =
mime_guess::from_path(file_path.clone()).first_or(default_mime);
part =
part.mime_str(mime.essence_str()).map_err(|e| e.to_string())?;
part = match part.mime_str(mime.essence_str()) {
Ok(p) => p,
Err(e) => {
return Ok(response_err(
&*response.lock().await,
format!("Invalid mime for multi-part entry {e:?}"),
window,
)
.await);
}
};
}

// Set file path if not empty
Expand Down
3 changes: 2 additions & 1 deletion src-tauri/src/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use crate::{DEFAULT_WINDOW_HEIGHT, DEFAULT_WINDOW_WIDTH, MIN_WINDOW_HEIGHT, MIN_
use log::{info, warn};
use std::process::exit;
use tauri::{
AppHandle, Emitter, LogicalSize, Manager, Runtime, TitleBarStyle, WebviewUrl, WebviewWindow,
AppHandle, Emitter, LogicalSize, Manager, Runtime, WebviewUrl, WebviewWindow,
};
use tauri_plugin_opener::OpenerExt;
use tokio::sync::mpsc;
Expand Down Expand Up @@ -67,6 +67,7 @@ pub(crate) fn create_window<R: Runtime>(
if config.hide_titlebar {
#[cfg(target_os = "macos")]
{
use tauri::TitleBarStyle;
win_builder = win_builder.hidden_title(true).title_bar_style(TitleBarStyle::Overlay);
}
#[cfg(not(target_os = "macos"))]
Expand Down
2 changes: 0 additions & 2 deletions src-web/components/GraphQLEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { useEffect, useMemo, useRef, useState } from 'react';
import { useLocalStorage } from 'react-use';
import { useIntrospectGraphQL } from '../hooks/useIntrospectGraphQL';
import { showDialog } from '../lib/dialog';
import { tryFormatJson } from '../lib/formatters';
import { Button } from './core/Button';
import { Dropdown } from './core/Dropdown';
import type { EditorProps } from './core/Editor/Editor';
Expand Down Expand Up @@ -178,7 +177,6 @@ export function GraphQLEditor({ request, onChange, baseRequest, ...extraEditorPr
Variables
</Separator>
<Editor
format={tryFormatJson}
language="json"
heightMode="auto"
defaultValue={currentBody.variables}
Expand Down
2 changes: 0 additions & 2 deletions src-web/components/GrpcEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import { useEffect, useMemo, useRef } from 'react';
import type { ReflectResponseService } from '../hooks/useGrpc';
import { showAlert } from '../lib/alert';
import { showDialog } from '../lib/dialog';
import { tryFormatJson } from '../lib/formatters';
import { pluralizeCount } from '../lib/pluralize';
import { Button } from './core/Button';
import type { EditorProps } from './core/Editor/Editor';
Expand Down Expand Up @@ -186,7 +185,6 @@ export function GrpcEditor({
useTemplating
forceUpdateKey={request.id}
defaultValue={request.message}
format={tryFormatJson}
heightMode="auto"
placeholder="..."
ref={editorViewRef}
Expand Down
4 changes: 1 addition & 3 deletions src-web/components/RequestPane.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { HttpRequest } from '@yaakapp-internal/models';
import type {GenericCompletionOption} from "@yaakapp-internal/plugins";
import type { GenericCompletionOption } from '@yaakapp-internal/plugins';
import classNames from 'classnames';
import { atom, useAtom, useAtomValue } from 'jotai';
import { atomWithStorage } from 'jotai/utils';
Expand All @@ -21,7 +21,6 @@ import { useUpdateAnyHttpRequest } from '../hooks/useUpdateAnyHttpRequest';
import { deepEqualAtom } from '../lib/atoms';
import { languageFromContentType } from '../lib/contentType';
import { fallbackRequestName } from '../lib/fallbackRequestName';
import { tryFormatJson } from '../lib/formatters';
import { generateId } from '../lib/generateId';
import {
BODY_TYPE_BINARY,
Expand Down Expand Up @@ -407,7 +406,6 @@ export const RequestPane = memo(function RequestPane({
defaultValue={`${activeRequest.body?.text ?? ''}`}
language="json"
onChange={handleBodyTextChange}
format={tryFormatJson}
stateKey={`json.${activeRequest.id}`}
/>
) : activeRequest.bodyType === BODY_TYPE_XML ? (
Expand Down
3 changes: 2 additions & 1 deletion src-web/components/SidebarItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ export const SidebarItem = memo(function SidebarItem({

const itemPrefix = (item.model === 'http_request' || item.model === 'grpc_request') && (
<HttpMethodTag
shortNames
request={item}
className={classNames(!(active || selected) && 'text-text-subtlest')}
/>
Expand Down Expand Up @@ -271,7 +272,7 @@ export const SidebarItem = memo(function SidebarItem({
onKeyDown={handleInputKeyDown}
/>
) : (
<span className="truncate">{itemName}</span>
<span className="truncate w-full">{itemName}</span>
)}
</div>
{latestGrpcConnection ? (
Expand Down
12 changes: 11 additions & 1 deletion src-web/components/core/Editor/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { useRequestEditor } from '../../../hooks/useRequestEditor';
import { useSettings } from '../../../hooks/useSettings';
import { useTemplateFunctionCompletionOptions } from '../../../hooks/useTemplateFunctions';
import { showDialog } from '../../../lib/dialog';
import { tryFormatJson, tryFormatXml } from '../../../lib/formatters';
import { TemplateFunctionDialog } from '../../TemplateFunctionDialog';
import { TemplateVariableDialog } from '../../TemplateVariableDialog';
import { IconButton } from '../IconButton';
Expand Down Expand Up @@ -134,7 +135,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
}

if (disabled) {
readOnly = true;
readOnly = true;
}

if (
Expand All @@ -147,6 +148,15 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
disableTabIndent = true;
}

if (format == null) {
format =
language === 'json'
? tryFormatJson
: language === 'xml' || language === 'html'
? tryFormatXml
: undefined;
}

const cm = useRef<{ view: EditorView; languageCompartment: Compartment } | null>(null);
useImperativeHandle(ref, () => cm.current?.view, []);

Expand Down
12 changes: 5 additions & 7 deletions src-web/components/core/HttpMethodTag.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,26 +15,24 @@ const longMethodMap = {
delete: 'DELETE',
options: 'OPTIONS',
head: 'HEAD',
grpc: 'GRPC',
} as const;

const shortMethodMap: Record<keyof typeof longMethodMap, string> = {
get: 'GET',
put: 'PUT',
post: 'POST',
patch: 'PTCH',
post: 'PST',
patch: 'PTC',
delete: 'DEL',
options: 'OPTS',
head: 'HEAD',
grpc: 'GRPC',
options: 'OPT',
head: 'HED',
};

export function HttpMethodTag({ shortNames, request, className }: Props) {
const method =
request.model === 'http_request' && request.bodyType === 'graphql'
? 'GQL'
: request.model === 'grpc_request'
? 'GRPC'
? 'GRP'
: request.method;

const m = method.toLowerCase();
Expand Down
83 changes: 81 additions & 2 deletions src-web/components/core/PairEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { formatSize } from '@yaakapp-internal/lib/formatSize';
import classNames from 'classnames';
import type { EditorView } from 'codemirror';
import {
Expand All @@ -13,6 +14,8 @@ import {
import type { XYCoord } from 'react-dnd';
import { useDrag, useDrop } from 'react-dnd';
import { useToggle } from '../../hooks/useToggle';
import { languageFromContentType } from '../../lib/contentType';
import { showDialog } from '../../lib/dialog';
import { generateId } from '../../lib/generateId';
import { showPrompt } from '../../lib/prompt';
import { DropMarker } from '../DropMarker';
Expand All @@ -21,6 +24,8 @@ import { Button } from './Button';
import { Checkbox } from './Checkbox';
import type { DropdownItem } from './Dropdown';
import { Dropdown } from './Dropdown';
import type { EditorProps } from './Editor/Editor';
import { Editor } from './Editor/Editor';
import type { GenericCompletionConfig } from './Editor/genericCompletion';
import { Icon } from './Icon';
import { IconButton } from './IconButton';
Expand Down Expand Up @@ -326,6 +331,7 @@ function PairEditorRow({
const ref = useRef<HTMLDivElement>(null);
const nameInputRef = useRef<EditorView>(null);
const valueInputRef = useRef<EditorView>(null);
const valueLanguage = languageFromContentType(pair.contentType ?? null);

useEffect(() => {
if (forceFocusNamePairId === pair.id) {
Expand Down Expand Up @@ -380,6 +386,24 @@ function PairEditorRow({
[onChange, pair],
);

const handleEditMultiLineValue = useCallback(
() =>
showDialog({
id: 'pair-edit-multiline',
size: 'dynamic',
title: <>Edit {pair.name}</>,
render: ({ hide }) => (
<MultilineEditDialog
hide={hide}
onChange={handleChangeValueText}
defaultValue={pair.value}
language={valueLanguage}
/>
),
}),
[handleChangeValueText, pair.name, pair.value, valueLanguage],
);

const [, connectDrop] = useDrop<Pair>(
{
accept: ItemTypes.ROW,
Expand Down Expand Up @@ -495,6 +519,15 @@ function PairEditorRow({
onFocus={handleFocus}
placeholder={valuePlaceholder ?? 'value'}
/>
) : pair.value.includes('\n') ? (
<Button
color="secondary"
size="sm"
onClick={handleEditMultiLineValue}
title={pair.value}
>
Edit {formatSize(pair.value.length)}
</Button>
) : (
<Input
ref={valueInputRef}
Expand All @@ -505,6 +538,7 @@ function PairEditorRow({
size="sm"
containerClassName={classNames(isLast && 'border-dashed')}
validate={valueValidate}
language={valueLanguage}
forceUpdateKey={forceUpdateKey}
defaultValue={pair.value}
label="Value"
Expand All @@ -526,6 +560,7 @@ function PairEditorRow({
onChangeText={handleChangeValueText}
onChangeContentType={handleChangeValueContentType}
onDelete={handleDelete}
editMultiLine={handleEditMultiLineValue}
/>
) : (
<Dropdown items={deleteItems}>
Expand All @@ -552,12 +587,14 @@ function FileActionsDropdown({
onChangeText,
onChangeContentType,
onDelete,
editMultiLine,
}: {
pair: Pair;
onChangeFile: ({ filePath }: { filePath: string | null }) => void;
onChangeText: (text: string) => void;
onChangeContentType: (contentType: string) => void;
onDelete: () => void;
editMultiLine: () => void;
}) {
const onChange = useCallback(
(v: string) => {
Expand All @@ -569,10 +606,15 @@ function FileActionsDropdown({

const extraItems = useMemo<DropdownItem[]>(
() => [
{
label: 'Edit Multi-Line',
leftSlot: <Icon icon="file_code" />,
hidden: pair.isFile,
onSelect: editMultiLine,
},
{
label: 'Set Content-Type',
leftSlot: <Icon icon="pencil" />,
hidden: !pair.isFile,
onSelect: async () => {
const contentType = await showPrompt({
id: 'content-type',
Expand Down Expand Up @@ -602,7 +644,7 @@ function FileActionsDropdown({
leftSlot: <Icon icon="trash" />,
},
],
[onChangeContentType, onChangeFile, onDelete, pair.contentType, pair.isFile],
[editMultiLine, onChangeContentType, onChangeFile, onDelete, pair.contentType, pair.isFile],
);

return (
Expand All @@ -629,3 +671,40 @@ function emptyPair(): PairWithId {
function isPairEmpty(pair: Pair): boolean {
return !pair.name && !pair.value;
}

function MultilineEditDialog({
defaultValue,
language,
onChange,
hide,
}: {
defaultValue: string;
language: EditorProps['language'];
onChange: (value: string) => void;
hide: () => void;
}) {
const [value, setValue] = useState<string>(defaultValue);
return (
<div className="w-[100vw] max-w-[40rem] h-[50vh] max-h-full grid grid-rows-[minmax(0,1fr)_auto]">
<Editor
heightMode="auto"
defaultValue={defaultValue}
language={language}
onChange={setValue}
stateKey={null}
/>
<div>
<Button
color="primary"
className="ml-auto my-2"
onClick={() => {
onChange(value);
hide();
}}
>
Done
</Button>
</div>
</div>
);
}
Loading

0 comments on commit 662c38d

Please sign in to comment.